Асинхронная рансомварь. Разбираем механизм самых быстрых криптолокеров

D2

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

Что самое главное в любом вирусе‑шифровальщике? Конечно же, его скорость! Наиболее эффективных показателей возможно достичь с помощью механизма IOCP, и его используют все популярные локеры: LockBit, Conti, Babuk. Давай посмотрим, как работает этот механизм, чтобы узнать, как действуют вирусописатели. Эти знания пригодятся нам при реверсе.

Синхронный и асинхронный ввод-вывод​

Windows поддерживает два способа работы с файлами: синхронный и асинхронный.

Под синхронным способом ввода‑вывода понимаются стандартные функции ReadFile() и WriteFile() и иные, отличные от WinAPI, которые долго, нудно и постепенно читают данные из файла. Единственная их особенность — поток простаивает до тех пор, пока операция не будет завершена. Вызвал ReadFile() для чтения 10 Гбайт, и сиди жди, пока файл считается. К такому же поведению можно отнести функции из std::ifstream. Само собой, есть вариант считывания файла построчно, но временные показатели не особенно изменятся. Не исключено, что даже увеличатся.

Если шифровальщик будет таким образом перебирать все файлы в системе, то его эффективность окажется невысокой. Свежеустановленная винда занимает около 50 Гбайт, а со всем софтом на диске в среднем будет занято в районе 300 Гбайт. Если пользователь склонен к накопительству, то в несколько раз больше.

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

Замеряем скорость чтения​

Я набросал небольшую программку, которая поможет тебе измерять скорость чтения файлов. Ее полный код — ниже.
C++: Скопировать в буфер обмена
Код:
#include <iostream>
#include <fstream>
#include <vector>
#include <chrono>
int main() {
  std::string filename = "D:\\ISO\\debian-11.7.0-amd64-netinst.iso";
  std::ifstream file(filename, std::ios::binary);
  if (!file.is_open()) {
    std::cerr << "Can't open file: " << filename << std::endl;
    return 1;
  }
  file.seekg(0, std::ios::end);
  std::streampos fileSize = file.tellg();
  file.seekg(0, std::ios::beg);
  std::vector<char> buffer(fileSize);
  auto start = std::chrono::high_resolution_clock::now();
  file.read(buffer.data(), buffer.size());
  auto end = std::chrono::high_resolution_clock::now();
  std::chrono::duration<double> duration = end - start;
  file.close();
  if (!file) {
    std::cerr << "Reading error" << std::endl;
    return 1;
  }
  std::cout << "Elapsed time: " << duration.count() << " sec." << std::endl;
  for (size_t i = 0; i < 10 && i < buffer.size(); ++i) {
    std::cout << std::hex << static_cast<int>(buffer[i] & 0xff) << " ";
  }
  std::cout << std::dec << std::endl;
  return 0;
}
Авторы вирусов‑шифровальщиков все чаще обращаются к асинхронному вводу‑выводу. Для этого обычно используются функции ReadFileEx() и WriteFileEx(). Да, то же в теории можно делать и с обычными ReadFile() и WriteFile(), но пользоваться расширенными функциями удобнее. При вызове этих функций со специальными параметрами они сразу же вернут поток управления, и программа не будет простаивать. Вызвал ReadFileEx() для файла размером 10 Гбайт, и продолжай работу потока. Как только файл считался, получишь уведомление.

Сам асинхронный ввод‑вывод делится на несколько подвидов:
  • Multithreaded I/O — вариант для консерваторов. Никакой асинхронщины тут на самом деле нет: разработчик создает множество потоков в программе, каждый из которых синхронно считывает или записывает данные. Представь, что есть десять файлов по 10 Гбайт, вместо считывания их в одном потоке ты считываешь их в десяти параллельных потоках, что ускоряет время операции в десять раз;
  • Overlapped I/O — асинхронщина в чистом ее виде. При чтении и записи поток не ждет завершения операции, а продолжает свою работу. Поток может приостановиться по собственному желанию только тогда, когда ему потребуется результат операции ввода‑вывода. Реализуется такой метод с помощью специальной структуры OVERLAPPED, отсюда и название;
  • Completion Routines I/O (extended I/O) — отличается от Overlapped I/O тем, что при завершении ввода‑вывода система вызовет специальную callback-функцию. Чаще всего реализуется через APC (asynchronous procedure calls) либо IOCP (input-output completion port). О втором варианте мы и будем говорить.

Что следует учитывать при работе с асинхронным I/O​

Стоит помнить, что асинхронные операции сразу же возвращают поток управления, а считывание (например, при вызове ReadFileEx()) происходит в фоновом режиме. Как следствие, нельзя:
  • отталкиваться от возвращаемого функцией значения до завершения асинхронной операции;
  • использовать количество переданных байтов или данные из структуры OVERLAPPED в качестве критерия проверки до завершения асинхронной операции;
  • повторно использовать данные из структуры OVERLAPPED до завершения асинхронной операции.

Структура OVERLAPPED​

Запрос на асинхронный ввод‑вывод так или иначе предполагает, что будет использоваться вот эта структура.
C++: Скопировать в буфер обмена
Код:
typedef struct _OVERLAPPED {
  ULONG_PTR Internal;
  ULONG_PTR InternalHigh;
  union {
    struct {
      DWORD Offset;
      DWORD OffsetHigh;
    };
    PVOID Pointer;
    };
  HANDLE hEvent;
} OVERLAPPED, *LPOVERLAPPED;
  • Internal, InternalHigh — не трогаем, это поле хранит инфу для системы. Обычно там лежит значение STATUS_PENDING, если файл еще читается;
  • Offset, OffsetHigh — смещения, указывающие, с какого места должна начинаться асинхронная операция (Pointer — альтернатива этим полям. Особенно удобно использовать, если требуется 64-битное смещение);
  • hEvent — хендл события, которое переходит в свободное (сигнальное) состояние после завершения асинхронной операции. Поэтому завершение асинхронной операции можно отслеживать, например с помощью WaitForSingeObjectEx().
При асинхронных операциях именно в этой структуре устанавливается файловый указатель. Допустим, если бы мы хотели прочитать только вторую половину файла, то сделали бы так.
C++: Скопировать в буфер обмена
Код:
OVERLAPPED ovlp;
...
// Установка позиции начала чтения в файле
ovlp.Offset=N*2;
ReadFile(hFile, pBuffer, N * 2, &dwCount, &ovlp);

Как видишь, структура явно передается в вызовах ReadFile() и WriteFile(). Главное — не забыть при получении хендла файла, например с помощью вызова CreateFile() (который всегда синхронный), установить флаг FILE_FLAG_OVERLAPPED. В результате файл открывается в асинхронном режиме.

C++: Скопировать в буфер обмена
Код:
// Открытие файла
hFile = CreateFile(szFileName, GENERIC_READ, 0, NULL,
  // Асинхронные операции
  OPEN_EXISTING, FILE_FLAG_OVERLAPPED,
           NULL);
Стоит отметить некоторые особенности:
  • экземпляр этой структуры должен сохраняться до тех пор, пока работает асинхронная операция;
  • событие сможет кто угодно поместить в сигнальное состояние. То есть тебя могут невольно обмануть, случайно сказав, что операция завершилась, поэтому будь осторожен.
Помимо этого, функции ReadFile() и WriteFile() в асинхронном режиме работы всегда возвращают FALSE, что нормально. Чтобы понять, успешно ли началась асинхронная операция, нужно дернуть GetLastError(). Если эта функция отдает ERROR_IO_PENDING, то все отлично, асинхронная операция началась.

Еще у этих функций есть параметр bytes, который возвращает количество прочитанных данных. При асинхронной операции этот параметр не имеет значения, так как операция не завершена. Лучше указать NULL для этого аргумента. Вот пример кода, который открывает файл, асинхронно считывает данные, а затем ожидает завершения операции.

C++: Скопировать в буфер обмена
Код:
HANDLE hFile = ::CreateFile(L"c:\\temp\\mydata.txt", GENERIC_READ,  FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, nullptr);
if (hFile != INVALID_HANDLE_VALUE) {
  OVERLAPPED ov = { 0 };
  ov.hEvent = ::CreateEvent(nullptr, TRUE, FALSE, nullptr);
  BYTE buffer[1 << 12]; // 4 KB
  BOOL ok = ::ReadFile(hFile, buffer, sizeof(buffer), nullptr, &ov);
  if(!ok) {
    if (::GetLastError() != ERROR_IO_PENDING) {
    // Произошла ошибка
    return;
  }
  else {
    // Поток здесь может делать еще что-нибудь
    // Do();
    // Поток сделал все, что мог, теперь нужно данные получить из асинхронной операции
    // Синхронизация по событию
    ::WaitForSingleObject(ov.hEvent, INFINITE);
    ::CloseHandle(ov.hEvent);
  }
}
// Здесь можно работать с данными
::CloseHandle(hFile);

После завершения асинхронной операции обычно обращаются к функции GetOverlappedResult() для получения данных.
C++: Скопировать в буфер обмена
Код:
BOOL GetOverlappedResult(
_In_ HANDLE hFile,
_In_ LPOVERLAPPED lpOverlapped,
_Out_ LPDWORD lpNumberOfBytesTransferred,
_In_ BOOL bWait);
  • hFile — хендл файла, над которым была проведена асинхронная операция;
  • lpOverlapped — структура OVERLAPPED, используемая во время асинхронной операции;
  • lpNumberOfBytesTransferred — количество байтов, переданных/прочитанных во время асинхронной операции;
  • bWait — если указано TRUE, а также параметр Internal структуры OVERLAPPED равен STATUS_PENDING, то функция не вернет управление до тех пор, пока не будет завершена асинхронная операция. Если указать FALSE, а асинхронная операция еще выполняется, то функция вернет FALSE, а GetLastError() — значение ERROR_IO_INCOMPLETE. Если асинхронная операция завершена, то этот параметр не имеет значения.
Функция возвращает TRUE после завершения операции асинхронного I/O. Если функция вернула FALSE, значит, что‑то пошло не так, лови с помощью GetLastError().

Помимо этого, у нас есть возможность отменить асинхронную операцию с помощью CancelIo().
C++: Скопировать в буфер обмена
Код:
BOOL WINAPI CancelIo(
  _In_ HANDLE hFile
);
  • hFile — дескриптор файла, в который производится перекрывающий ввод‑вывод.
С помощью этой функции мы сможем отменить операцию асинхронного ввода‑вывода только текущего потока. Если попробуем передать в нее асинхронные функции других потоков, то ничего не произойдет.

Отмененные операции завершаются с ошибкой ERROR_OPERATION_ABORTED.

IOCP​

Представь, что у нас теперь не один файл, а миллион. В нашей программе совершается множество операций ввода‑вывода, и, как следствие, использовать примеры кода выше будет не очень удобно. Поэтому разработчики Windows создали IOCP.

IOCP — это механизм, который используется для оповещения о завершении асинхронных операций и позволяет одному или нескольким потокам обрабатывать результаты этих операций. То есть к IOCP цепляются всякие объекты и этот IOCP получает уведомления о том, что такая‑то асинхронная операция завершилась. После этого встроенный пул потоков в IOCP может обработать результат этих операций.

В IOCP есть такой термин — порт. Порт представляет собой очередь в ядре. Очередь заполняется записями, когда завершается асинхронная операция. К этому порту привязано определенное количество потоков, которые начинают обрабатывать очередь. Разработчик может регулировать количество потоков.

Вот для наглядности картинка со StackOverflow.

1725527111004.png


Здесь
  • completion notifications — поступающие данные о завершении работы асинхронной операции. Также иногда называется I/O completion request;
  • completion port — место, куда попадают данные;
  • queued thread — потоки, которые находятся в «спящем» состоянии. У них еще нет работы, например когда очередь пустая;
  • worker thread — поток, получивший задачу из completion port.

IOCP как очередь в больницу​

Если по‑прежнему не очень понятно, то представь больницу с кучей пенсионерок. Но приходят они не сразу, а время от времени. Больница — completion port, бабушки — результаты асинхронных операций, свободные врачи — queued thread, врачи, уже работающие с пациентами, — worker thread.

Наступило утро, пришли врачи, открылась больница (функция CreateIOCompletionPort()), бабушек нет, врачи в queued thread. Потом приехал первый автобус и выгрузил толпу бабушек (либо автоматически, либо через PostQueuedCompletionPort()). Врачи сразу же расхватали пациенток (GetQueuedCompletionStatus()) и превратились в worker thread.

Следом приезжает маршрутка, и вновь пенсионерки заходят в больницу. Но все врачи заняты, поэтому старушки покорно ждут, когда какой‑нибудь врач освободится, то есть worker thread завершит работу и перейдет в состояние queued thread, а из состояния queued thread можно брать новую бабушку.

И такой цикл будет повторяться до тех пор, пока результаты всех асинхронных операций, поступивших в пул потоков, не будут завершены. То есть все бабушки будут снабжены диагнозами и предписаниями и отправлены по домам.

Помимо этого, пациенток давным‑давно привязали к этой конкретной больнице. Поэтому среди обилия работающих IOCP-портов бабушки будут приходить именно к нашему.

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

Создание IOCP и связывание с потоками, дескрипторами​

CreateIoCompletionPort()​

С помощью этой функции можно создать IOCP. Причем с помощью этой же функции можно присоединить к IOCP какой‑нибудь хендл.
C++: Скопировать в буфер обмена
Код:
HANDLE CreateIoCompletionPort(
  HANDLE FileHandle,
  HANDLE ExistingCompletionPort,
  DWORD CompletionKey,
  DWORD NumberOfConcurrentThreads
);
Здесь
  • FileHandle — OVERLAPPED-дескриптор, присоединяемый к порту IOCP (допустим, пайп с FILE_FLAG_OVERLAPPED или сокет, созданный обычной функцией socket()). Если задать INVALID_DESCRIPTOR_HANDLE, то функция просто создаст новый порт IOCP и вернет его дескриптор, в этом случае ExistingCompletionPort должен быть равен NULL;
  • ExistingCompletionPort — используется для добавления хендла в порт. То есть в нашей аналогии — чтобы привязать бабушку к конкретной больнице. В этом параметре указывается больница, к которой привязать бабушку из FileHandle;
  • CompletionKey — специальное значение, называемое ключом. Это указатель на любые данные, которые мы сами определили. Используется в основном для того, чтобы внутри потока, который начал обрабатывать запись из IOCP, мы могли отличить одну операцию от другой. Или, например, одну бабушку от другой. Обычно в качестве ключа используется указатель на структуру данных, которая содержит тип операции, дескриптор и буфер данных. Буфер данных — что угодно. Но в минимальном виде можно использовать и просто один байт, который будет показывать, что за асинхронная операция случилась (чтение, запись или что‑то другое). Этот буфер данных можно получить через GetQueuedCompletionStatus(), он будет лежать в параметре lpCompletionKey;
  • NumberOfConcurrentThreads — предельно допустимое количество потоков, которым разрешено параллельное выполнение. Если этот параметр равен нулю, то в качестве предела используется количество процессоров, установленных в системе. В случае если мы добавляем в IOCP какой‑то дескриптор, это поле игнорируется.
Итак, сначала следует создать сам IOCP-порт, то есть больницу. Делается вот так:
C++: Скопировать в буфер обмена
HANDLE iocp=CreateIoCompletionPort(INVALID_HANDLE_VALUE,0,0,0);

Следующий шаг — создание потоков для пула потоков, то есть создание врачей. Сколько делать — решает программист, все зависит от задачи и потребностей (количества бабушек). Обычно число процессоров на системе равно числу потоков в пуле. Но это значение можно и менять.
C++: Скопировать в буфер обмена
Код:
struct ThreadData {
  HANDLE hIOCP;
};
ThreadData threadData;
threadData.hIOCP = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, iocpThreadCount);
if (threadData.hIOCP == NULL) {
  std::wcerr << L"CreateIoCompletionPort failed with error: " << GetLastError() << std::endl;
  return 1;
}
HANDLE hThreads[iocpThreadCount];
for (int i = 0; i < iocpThreadCount; ++i) {
  hThreads[i] = CreateThread(NULL, 0, WorkerThread, &threadData, 0, NULL);
  if (hThreads[i] == NULL) {
    std::wcerr << L"CreateThread failed with error: " << GetLastError() << std::endl;
    return 1;
  }
}

Вот чуть более простой пример, без структуры и массива.
C++: Скопировать в буфер обмена
Код:
for(int i=1;i<=2;i++)
{
  HANDLE hWorking=CreateThread(0,0,(LPTHREAD_START_ROUTINE)&WorkingThread,iocp,0,0);
  CloseHandle(hWorking);
}
Обрати внимание, что мы здесь четвертым параметром передаем хендл нашего IOCP, чтобы связать конкретный поток с пулом потоков в IOCP. Этот хендл понадобится потокам, когда они начнут заявлять о своей готовности начать работать. Этот хендл будет передан в функцию WorkingThread() как lParam и в дальнейшем использован в GetQueuedCompletionStatus()

Полноценные примеры кода, где я продемонстрирую эту и следующие функции, будут в конце статьи.

Присоединение потока к пулу потоков​

GetQueuedCompletionStatus()​

Эта функция нужна, чтобы присоединить текущий поток (который ее вызывает) к пулу потоков. Обычно она вызывается из callback-функции потока.
C++: Скопировать в буфер обмена
Код:
BOOL GetQueuedCompletionStatus(
  HANDLE CompletionPort,
  LPDWORD lpNumberOfBytes,
  PULONG_PTR lpCompletionKey,
  LPOVERLAPPED *lpOverlapped,
  DWORD dwMilliseconds);
Именно на этой функции поток из пула потоков IOCP будет спать до тех пор, пока не получит IRP-сообщение. А после получения приступит к обработке записи из очереди IOCP.
Здесь
  • CompletionPort — IOCP-порт, к пулу потоков которого присоединять поток;
  • lpNumberOfBytes — указатель на переменную, в которую запишется количество переданных байтов в результате завершения асинхронной операции. Эти данные заполняются автоматически и присутствуют в записи, которая добавляется в очередь IOCP. Если записей в очереди IOCP нет, то мы можем вручную добавить новую запись в IOCP через PostQueuedCompletionStatus();
  • lpCompletionKey — ключ завершения. По нему, чисто теоретически, можно определять, что за файл завершил работу, так как этот ключ указывается для каждого файла при вызове CreateIoCompletionPort(). Эти данные заполняются автоматически и присутствуют в записи, которая добавляется в очередь IOCP. Если записей в IOCP-очереди нет, то мы можем вручную добавить новую запись в IOCP через PostQueuedCompletionStatus(). Причем через этот параметр можно получать и кастомные структуры. Например, у нас есть следующая структура, которую мы используем для получения информации о том, над каким файлом операция асинхронного чтения завершилась.
  • dwMilliseconds — время, на которое может уснуть поток в ожидании появления в очереди IOCP какой‑нибудь записи. INFINITE — бесконечно.
C++: Скопировать в буфер обмена
Код:
struct IOData
{
  OVERLAPPED overlapped;
  HANDLE fileHandle;
  wchar_t buffer[dwReadBufferSize] = { 0 };
  wchar_t filePath[MAX_PATH] = { 0 };
  DWORD bytesRead;
};
Обрати внимание, что первым параметром указывается сама структура OVERLAPPED. Эти данные заполняются перед вызовом асинхронной операции и перед тем, как файловый хендл будет связан с IOCP.
C++: Скопировать в буфер обмена
Код:
if (hFile != INVALID_HANDLE_VALUE)
{
 IOData* pIOData = new IOData{};
 memset(pIOData, 0, sizeof(IOData));
 pIOData->fileHandle = hFile;
 wcscpy_s(pIOData->filePath, MAX_PATH, findFileData.cFileName);
 CreateIoCompletionPort(hFile, hIOCP, (ULONG_PTR)pIOData, 0);
 ReadFile(hFile, pIOData->buffer, dwReadBufferSize, NULL, &pIOData->overlapped);
}
Парсинг структуры происходит тут, в функции потока из пула потоков.
C++: Скопировать в буфер обмена
Код:
DWORD WINAPI WorkerThread(LPVOID lpParam)
{
  HANDLE hIOCP = (HANDLE)lpParam;
  DWORD bytesTransferred;
  ULONG_PTR key;
  LPOVERLAPPED lpOverlapped;
  IOData* pData = nullptr;
  while (GetQueuedCompletionStatus(hIOCP, &bytesTransferred, &key, &lpOverlapped, INFINITE))
  {
    // Парсинг
    pData = (IOData*)key;
  }
  return 0;
}
  • lpOverlapped — указатель на OVERLAPPED-структуру, ассоциированную с этой операцией ввода‑вывода. Эти данные заполняются автоматически и присутствуют в записи, которая добавляется в очередь IOCP. Если записей очереди в IOCP нет, то мы можем вручную добавить новую запись через PostQueuedCompletionStatus();

Фактически, вызывая эту функцию, мы засовываем поток (врача) в пул потоков IOCP (больницу) и приостанавливаем его выполнение до тех пор, пока не появится какая‑нибудь запись в очереди IOCP (пока не придет бабушка). Причем функция вернет поток управления только после появления в очереди новых записей. Мы можем пробудить потоки, если отправим сообщение вручную, например через PostQueuedCompletionStatus().

Отправка сообщения I/O completion packet вручную​

PostQueuedCompletionStatus()​

Представь, что у нас есть какая‑то элитная бабушка, которую нужно принудительно записать в очередь. В таком случае используется PostQueuedCompletionStatus() — функция, позволяющая вручную добавить запись в очередь IOCP.
C++: Скопировать в буфер обмена
Код:
BOOL PostQueuedCompletionStatus(
  HANDLE CompletionPort,
  DWORD dwNumberOfBytesTransferred,
  ULONG_PTR dwCompletionKey,
  LPOVERLAPPED lpOverlapped);
  • CompletionPort — порт IOCP, на который отправлять I/O completion packet;
  • dwNumberOfBytesTransferred — значение, которое попадает в функции GetQueuedCompletionStatus() в переменную lpNumberOfBytesTransferred;
  • dwCompletionKey — ключ завершения. Значение, которое попадает в функции GetQueuedCompletionStatus() в переменную lpCompletionKey;
  • lpOverlapped — OVERLAPPED-структура. Значение, которое попадает в функции GetQueuedCompletionStatus() в переменную lpOverlapped.
Для пробуждения ожидающих потоков, когда нет завершившихся операций, иногда отсылают фиктивное значение ключа, например -1. Ожидающие потоки должны проверять значения ключей, и таким образом можно просигнализировать потоку, что пора прекратить работу.

Пример псевдошифровальщика​

Думаю, мы готовы реализовывать порт IOCP с пулом потоков. Ты будешь удивлен, но код небольшой. Логика его проста: асинхронное чтение файлов из папки с последующим выводом первых пяти байтов (собственно, поэтому я и назвал программу псевдошифровальщиком). Все параметры контролируются в глобальных переменных, поэтому добавить простейшую XOR-операцию с последующим WriteFile(), думаю, не составит труда.

Я решил не разбирать каждую строку, а добавить достаточно понятные комментарии в код.
C++: Скопировать в буфер обмена
Код:
#include <Windows.h>
#include <iostream>
#include <locale.h>
#include <string>
CRITICAL_SECTION gCritSection;
// Четыре треда в пуле
CONST DWORD iocpThreadCount = 4;
// Читаем 5 байт
CONST DWORD dwReadBufferSize = 5;
CONST std::wstring folder = L"C:\\Users\\Michael\\Downloads";
struct IOData
{
  OVERLAPPED overlapped;
  HANDLE fileHandle;
  wchar_t buffer[dwReadBufferSize] = { 0 };
  wchar_t filePath[MAX_PATH] = { 0 };
  DWORD bytesRead;
};
DWORD WINAPI WorkerThread(LPVOID lpParam)
{
  HANDLE hIOCP = (HANDLE)lpParam;
  DWORD bytesTransferred;
  ULONG_PTR key;
  LPOVERLAPPED lpOverlapped;
  IOData* pData = nullptr;
  // Ждем бабушку тут. Врачи в состоянии queued thread
  while (GetQueuedCompletionStatus(hIOCP, &bytesTransferred, &key, &lpOverlapped, INFINITE))
  {
    // Пришла бабушка
    EnterCriticalSection(&gCritSection);
    if (key == 0)
    {
      // Это была не бабушка, а главврач. Работа закончена!
      std::wcout << L"Thread in IOCP 0 records, received manual IRP from PostQueued. Exiting" << std::endl;
      LeaveCriticalSection(&gCritSection);
      break;
    }
    pData = (IOData*)key;
    if (bytesTransferred > 0)
    {
      std::wcout << L"File: " << pData->filePath << std::endl;
      std::wcout << L"Bytes: ";
      for (DWORD i = 0; i < 5; ++i) {
        std::wcout << static_cast<int>(pData->buffer[i]) << L" ";
      }
      std::wcout << std::endl;
    }
    else
    {
      std::wcerr << L"IO Failed or transferred 0 bytes" << std::endl;
    }
    LeaveCriticalSection(&gCritSection);
    CloseHandle(pData->fileHandle);
    // Бабушка обслужена. Идем к следующей
    delete pData;
  }
  return 0;
}
VOID ProcessFolder(const std::wstring& folder, HANDLE hIOCP)
{
  WIN32_FIND_DATAW findFileData;
  HANDLE hFind = FindFirstFileW((folder + L"\\*").c_str(), &findFileData);
  if (hFind != INVALID_HANDLE_VALUE)
  {
    do
    {
      if (!(findFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY))
      {
        HANDLE hFile = CreateFileW((folder + L"\\" + findFileData.cFileName).c_str(), GENERIC_READ,
          FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL);
        if (hFile != INVALID_HANDLE_VALUE)
        {
          IOData* pIOData = new IOData{};
          memset(pIOData, 0, sizeof(IOData));
          pIOData->fileHandle = hFile;
          wcscpy_s(pIOData->filePath, MAX_PATH, findFileData.cFileName);
      // Прикрепляем бабушку к больнице
          CreateIoCompletionPort(hFile, hIOCP, (ULONG_PTR)pIOData, 0);
      // Триггер асинхронной операции
      // По завершении функция GetQueuedCompletionStatus() вернет потоку поток управления
          ReadFile(hFile, pIOData->buffer, dwReadBufferSize, NULL, &pIOData->overlapped);
        }
        else
        {
          std::wcerr << L"Failed to open file: " << GetLastError() << std::endl;
        }
      }
      else
      {
        if (wcscmp(findFileData.cFileName, L".") != 0 && wcscmp(findFileData.cFileName, L"..") != 0)
        {
          ProcessFolder(folder + L"\\" + findFileData.cFileName, hIOCP);
        }
      }
    } while (FindNextFileW(hFind, &findFileData) != 0);
    FindClose(hFind);
  }
}
int main()
{
  InitializeCriticalSection(&gCritSection);
  setlocale(LC_ALL, "");
  // Создание больницы
  HANDLE hIOCP = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
  HANDLE hThreads[iocpThreadCount];
  for (DWORD i = 0; i < iocpThreadCount; i++)
  {
    // Добавление врача в больницу
    hThreads[i] = CreateThread(NULL, 0, WorkerThread, hIOCP, 0, NULL);
    if (hThreads[i] == NULL)
    {
      std::wcerr << L"CreateThread failed with error: " << GetLastError() << std::endl;
      return 1;
    }
  }
  // Пушинг бабушек
  ProcessFolder(folder, hIOCP);
  for (int i = 0; i < iocpThreadCount; i++)
  {
    // Завершение работы врачей
    PostQueuedCompletionStatus(hIOCP, 0, 0, NULL);
  }
  WaitForMultipleObjects(iocpThreadCount, hThreads, TRUE, INFINITE);
  for (HANDLE hThread : hThreads)
  {
    CloseHandle(hThread);
  }
  DeleteCriticalSection(&gCritSection);
  CloseHandle(hIOCP);
  return 0;
}
Пример работы:
1725527563743.png



Использование встроенного пула потоков​

BindIoCompletionCallback()​

Этот метод позволяет связать конкретный хендл с дефолтным пулом потоков, который присутствует в каждом процессе. Фактически вместо того, чтобы создавать собственный IOCP и пул потоков, мы используем готовый.
C++: Скопировать в буфер обмена
Код:
BOOL BindIoCompletionCallback(
  [in] HANDLE              FileHandle,
  [in] LPOVERLAPPED_COMPLETION_ROUTINE Function,
  [in] ULONG               Flags
);
Здесь
  • FileHandle — хендл, над которым проводят асинхронные операции;
  • Function — колбэк‑функция, которую нужно вызвать после выполнения асинхронной операции (I/O completion packet отправляется автоматически);
  • Flags — дополнительные флаги. Обычно 0.

Пример кода​

Вернемся к нашей программе из прошлой части статьи. Выведем первые пять байтов файлов из C:\Users\Michael\Downloads.
1725527666867.png


C++: Скопировать в буфер обмена
Код:
#include <windows.h>
#include <iostream>
#include <string>
#include <locale.h>
CRITICAL_SECTION ConsoleCriticalSection;
struct ReadContext {
  OVERLAPPED overlapped;
  HANDLE hFile;
  wchar_t buffer[5];
  wchar_t filePath[MAX_PATH];
};
void CALLBACK FileIOCompletionRoutine(DWORD dwErrorCode, DWORD dwNumberOfBytesTransfered, LPOVERLAPPED lpOverlapped) {
  ReadContext* context = reinterpret_cast<ReadContext*>(lpOverlapped);
  // Пришла новая бабушка. Добавляем критическую секцию для синхронизации вывода на консоль
  EnterCriticalSection(&ConsoleCriticalSection);
  if (dwErrorCode == 0 && dwNumberOfBytesTransfered > 0) {
    std::wcout << L"File path: " << context->filePath << std::endl;
    std::wcout << L"First 5 bytes: ";
    for (DWORD i = 0; i < dwNumberOfBytesTransfered / sizeof(wchar_t); ++i) {
      std::wcout << static_cast<int>(context->buffer[i]) << L" ";
    }
    std::wcout << std::endl;
  }
  else {
    std::wcerr << L"IO operation failed or transferred no bytes." << std::endl;
  }
  LeaveCriticalSection(&ConsoleCriticalSection);
  CloseHandle(context->hFile);
  delete context;
}
void InitiateRead(HANDLE hFile, const wchar_t* filePath) {
  ReadContext* context = new ReadContext();
  ZeroMemory(context, sizeof(ReadContext));
  wcscpy_s(context->filePath, filePath);
  if (BindIoCompletionCallback(hFile, FileIOCompletionRoutine, 0)) {
    // Связываем бабушку с больницей
    ReadFile(hFile, context->buffer, sizeof(context->buffer), NULL, &context->overlapped);
    // После завершения асинхронной операции бабушка придет в FileIOCompletionRoutine
  }
  else {
    CloseHandle(context->hFile);
    delete context;
  }
}
int wmain() {
  setlocale(LC_ALL, "");
  InitializeCriticalSection(&ConsoleCriticalSection);
  WIN32_FIND_DATA findFileData;
  HANDLE hFind = FindFirstFile(L"C:\\Users\\Michael\\Downloads\\*", &findFileData);
  if (hFind == INVALID_HANDLE_VALUE) {
    std::wcerr << L"FindFirstFile failed with error: " << GetLastError() << std::endl;
    return 1;
  }
  do {
    if (!(findFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) {
      std::wstring filePath = L"C:\\Users\\Michael\\Downloads\\" + std::wstring(findFileData.cFileName);
      HANDLE hFile = CreateFileW(filePath.c_str(), GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL);
      if (hFile == INVALID_HANDLE_VALUE) {
        continue;
      }
      // Начинаем асинхронную операцию
      InitiateRead(hFile, filePath.c_str());
    }
  } while (FindNextFile(hFind, &findFileData) != 0);
  FindClose(hFind);
  SleepEx(5000, TRUE);
  DeleteCriticalSection(&ConsoleCriticalSection);
  return 0;
}

Выводы​

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

Взято: ТУТ
 
Сверху Снизу