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

warning

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

 

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

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

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

Ес­ли шиф­роваль­щик будет таким обра­зом переби­рать все фай­лы в сис­теме, то его эффектив­ность ока­жет­ся невысо­кой. Све­жеус­танов­ленная вин­да занима­ет око­ло 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 I/O тем, что при завер­шении вво­да‑вывода сис­тема вызовет спе­циаль­ную callback-фун­кцию. Чаще все­го реали­зует­ся через APC (asynchronous procedure calls) либо IOCP (input-output completion port). О вто­ром вари­анте мы и будем говорить.
 

Что следует учитывать при работе с асинхронным 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);

Продолжение доступно только участникам

Материалы из последних выпусков становятся доступны по отдельности только через два месяца после публикации. Чтобы продолжить чтение, необходимо стать участником сообщества «Xakep.ru».

Присоединяйся к сообществу «Xakep.ru»!

Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее

  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    0 комментариев
    Межтекстовые Отзывы
    Посмотреть все комментарии