Содержание статьи
- Синхронный и асинхронный ввод-вывод
- Что следует учитывать при работе с асинхронным I/O
- Структура OVERLAPPED
- IOCP
- IOCP как очередь в больницу
- Создание IOCP и связывание с потоками, дескрипторами
- CreateIoCompletionPort()
- Присоединение потока к пулу потоков
- GetQueuedCompletionStatus()
- Отправка сообщения I/O completion packet вручную
- PostQueuedCompletionStatus()
- Пример псевдошифровальщика
- Использование встроенного пула потоков
- BindIoCompletionCallback()
- Пример кода
- Выводы
warning
Статья имеет ознакомительный характер и предназначена для специалистов по безопасности, проводящих расследование инцидентов и обратную разработку вредоносного ПО. Автор и редакция не несут ответственности за любой вред, причиненный с применением изложенной информации. Распространение вредоносных программ, нарушение работы систем и нарушение тайны переписки преследуются по закону.
Синхронный и асинхронный ввод-вывод
Windows поддерживает два способа работы с файлами: синхронный и асинхронный.
Под синхронным способом ввода‑вывода понимаются стандартные функции ReadFile(
и WriteFile(
и иные, отличные от WinAPI, которые долго, нудно и постепенно читают данные из файла. Единственная их особенность — поток простаивает до тех пор, пока операция не будет завершена. Вызвал ReadFile(
для чтения 10 Гбайт, и сиди жди, пока файл считается. К такому же поведению можно отнести функции из std::
. Само собой, есть вариант считывания файла построчно, но временные показатели не особенно изменятся. Не исключено, что даже увеличатся.
Если шифровальщик будет таким образом перебирать все файлы в системе, то его эффективность окажется невысокой. Свежеустановленная винда занимает около 50 Гбайт, а со всем софтом на диске в среднем будет занято в районе 300 Гбайт. Если пользователь склонен к накопительству, то в несколько раз больше.
Конечно, злоумышленники могут применить лайфхаки и шифровать не любые файлы, а только документы или только части файлов, но все равно при синхронном вводе‑выводе скорость будет низкой.
Замеряем скорость чтения
Я набросал небольшую программку, которая поможет тебе измерять скорость чтения файлов. Ее полный код — ниже.
#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
тем, что при завершении ввода‑вывода система вызовет специальную callback-функцию. Чаще всего реализуется через APC (asynchronous procedure calls) либо IOCP (input-output completion port). О втором варианте мы и будем говорить.I/ O
Что следует учитывать при работе с асинхронным I/O
Стоит помнить, что асинхронные операции сразу же возвращают поток управления, а считывание (например, при вызове ReadFileEx(
) происходит в фоновом режиме. Как следствие, нельзя:
- отталкиваться от возвращаемого функцией значения до завершения асинхронной операции;
- использовать количество переданных байтов или данные из структуры
OVERLAPPED
в качестве критерия проверки до завершения асинхронной операции; - повторно использовать данные из структуры
OVERLAPPED
до завершения асинхронной операции.
Структура OVERLAPPED
Запрос на асинхронный ввод‑вывод так или иначе предполагает, что будет использоваться вот эта структура.
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(
.)
При асинхронных операциях именно в этой структуре устанавливается файловый указатель. Допустим, если бы мы хотели прочитать только вторую половину файла, то сделали бы так.
OVERLAPPED ovlp;...// Установка позиции начала чтения в файлеovlp.Offset=N*2;ReadFile(hFile, pBuffer, N * 2, &dwCount, &ovlp);
Продолжение доступно только участникам
Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
Вариант 2. Открой один материал
Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.
Я уже участник «Xakep.ru»