50 минут руками — 5 минут автоматически. Или как я майнер удалял


Думаю, многие из вас знают, что такое майнер, а если нет, то вкратце — это программа, которая использует ресурсы компьютера для добычи (майнинга) криптовалюты. Сам по себе майнер не является вирусом, но есть те, кто им злоупотребляет, распространяя его скрытую вариацию под видом пиратского софта и сборок с играми.
В этой истории я расскажу, как столкнулся с таким майнером и потратил почти час на ручное удаление, как это позже удалось автоматизировать, и как уже из этого зародилась утилита по устранению таких майнеров.
С чем я столкнулся?
Примерно года 3 назад ко мне обратились с просьбой помочь освободить место на диске и убрать кучу хлама, который попал через рекламные установщики. Одно из приложений с непонятным названием категорически отказывалось удаляться. Как только я попытался открыть расположение этого процесса, проводник тут же закрылся. Ну ок, подумал, может я что‑то нажал, но с диспетчером задач поведение было аналогичным. Тогда стало понятно, что одной чисткой диска работа не ограничится.

Первые несколько минут я пытался найти и удалить зловреда вручную, но безуспешно. Ситуация усложнялась тем, что всё происходило удалённо, поэтому о безопасном режиме речи не было.
Скачать антивирусный софт вредонос не давал. Файл hosts оказался заблокирован. Блокнот закрывался при попытке его открыть. Знакомый скачал на телефон KVRT, перекинул на ПК, но запуск тоже не проходил.

Переименовываем файл и... получаем уже другую ошибку

Для дальнейшего анализа я выкачал две утилиты:
Autoruns — чтобы понять, что запускается вместе с системой.
Process Hacker (ныне System Informer) — продвинутая альтернатива Диспетчеру задач.
В Process Hacker я обнаружил подозрительные процессы, маскирующиеся под системные:

taskhostw.exe (тот самый Realtek HD Audio)
audiodg.exe
taskhost.exe,
winserv.exe,
microsofthost.exe
AppHost.exe и другие...
Как майнер защищался
1) Несколько копий зловреда запускаются одновременно и следят друг за другом. Один из процессов мог перезапускаться через службу или планировщик задач. Убийство одного процесса проблему не решает. Рабочий вариант — сначала приостановить подозрительные процессы, а уже потом завершить их.
2) Запрещающие групповые политики на запуск приложений из списка DisallowRun
3) Каталогам майнера выдаются явные запреты на полный доступ для текущего пользователя и системы, что как раз и не даёт нормально ставить антивирус.

Как я вычищал майнер вручную
Если процесс нельзя завершить, его можно приостановить. Такой функционал есть и в штатном мониторе ресурсов (resmon.exe), и в System Informer.

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

Удалить файлы всё ещё нельзя, потому что они заняты приостановленными процессами, зато их можно переименовать.

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

Перезагружаемся, проверяем процессы и файлы. Затем правим hosts. Теперь он не закрывается и нормально редактируется под админом.

Остались заблокированные каталоги антивирусов. Чтобы удалить такой каталог, нужно:
сделать текущего пользователя владельцем
удалить права доступа с типом «Запретить»
применить изменения
удалить каталог
И так для каждого заблокированного каталога.

Создатели этого зловреда явно понимали, что его будут удалять. После нескольких таких ручных удалений у других пользователей, примерно поняв, как он работает, время на его «выкорчёвывание» (по‑другому это не назвать) сократилось с 1 часа до 30 минут. Но тратить столько времени на один вирус — не дело. Нужно было как‑то автоматизировать этот процесс.
Автоматизация процесса
Для написания кода я отдал предпочтение C#. Он довольно прост в освоении и более менее с ним знаком, это первое. Второе — он смог дать нужный для меня функционал — приостанавливать процессы через WinAPI.
Алгоритм был примерно таким:
Найти и приостановить подозрительные процессы;
Почистить автозапуск: задачи планировщика, службы, ключи реестра, заблокированные каталоги, hosts;
Завершить приостановленные процессы;
Окончательно удалить файлы зловреда.
На этом этапе утилита представляла буквально небольшую софтину на с конкретным набором правил и действий над процессами.
За основу был взят MinerKiller. Идея простая: пройтись по каждому процессу, начислить баллы за подозрительные признаки и сравнить сумму с порогом.
В раннем прототипе учитывались, например:
Наличие pool, stratum и похожих маркеров;
Характерные аргументы командной строки;
Сетевые признаки, связанные с майнингом;
Схожее имя файла с системным.
В первых версиях я экспериментировал с разными весами и порогами. В результате получилась целочисленная шкала риска. На определённом уровне процесс считался достаточно подозрительным для приостановки. Но у такого подхода есть очевидный недостаток: эвристика не знает, что перед ней действительно вредоносная программа. Она лишь собирает косвенные признаки. Появились отдельные анализаторы для разных типов объектов, дополнительные проверки и возможность не удалять сомнительный объект сразу.
Логирование
Консоль при сканировании проскакивает очень быстро, поэтому здесь не обойтись одним Console.WriteLine. В класс Logger добавился метод WriteLog, который пишет одновременно в консоль и файл.
Первая версия была очень простой. На каждую строку лога заново открывался StreamWriter, записывалась строка и файл закрывался. На скане с тысячами строк это заметно тормозило. В текущем варианте файл открывается один раз при инициализации, а запись защищена синхронизацией доступа потоков:
internal static readonly object _logLock = new object();
static StreamWriter _writer;
public static void InitLogger(bool logSuppression)
{
if (!logSuppression)
{
_writer = new StreamWriter(
Path.Combine(LogsFolder, logFileName),
true,
new UTF8Encoding(false));
_writer.AutoFlush = true;
}
}
public static void WriteLog(string text, ConsoleColor color)
{
lock (_logLock)
{
Console.ForegroundColor = color;
Console.WriteLine($"[{DateTime.Now}]: {text}");
Console.ResetColor();
_writer?.WriteLine($"[{DateTime.Now}]: {text}");
}
}Заодно решились две мелочи: цвет консоли восстанавливается через Console.ResetColor(), а повторяющиеся строки не засоряют лог.
Был забавный момент с каталогом, содержащий логи. Он сам может оказаться заблокирован вредоносом. Поэтому перед записью с него снимаются атрибуты и ACL — тем же механизмом, которым лечатся каталоги майнера.
Приостановка процессов
Теперь прототип метода для приостановки. Объявляем необходимые WinAPI‑функции:
[DllImport("kernel32.dll")]
static extern IntPtr OpenThread(
ThreadAccess dwDesiredAccess,
bool bInheritHandle,
uint dwThreadId);
[DllImport("kernel32.dll")]
static extern uint SuspendThread(IntPtr hThread);
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool CloseHandle(IntPtr handle);
private static void SuspendProcess(int pid)
{
Process process = Process.GetProcessById(pid);
foreach (ProcessThread thread in process.Threads)
{
IntPtr handle = OpenThread(
ThreadAccess.SUSPEND_RESUME,
false,
(uint)thread.Id);
if (handle == IntPtr.Zero)
continue;
SuspendThread(handle);
CloseHandle(handle);
}
}Приостановить процесс целиком можно разными способами. Я выбрал вариант, где нужно пройтись по потокам и вызвать SuspendThread.
Для работы с чужими нужны соответствующие привилегии. В зависимости от выполняемой операции утилита использует SeDebugPrivilege, SeBackupPrivilege, SeRestorePrivilege, SeTakeOwnershipPrivilege и SeSecurityPrivilege.
В месте, где процесс достигает порога риска, вызывается приостановка. При этом найденные процессы не завершаются по одному. Сначала собирается список, а затем процессы завершаются вместе.
Как прототип из MinerKiller превратился в MinerSearch
Первая доработанная версия могла удалять только один конкретный майнер. Естесственно были и другие въедливые версии майнеров, которые на тот момент никак не распознавались. Это привело к решению расширять функционал.
Появлялись новые типы объектов: ключи реестра, задачи планировщика, службы, WMI, правила брандмауэра, куча правил детекта и так далее. В какой‑то момент всё это начало жить в одном большом классе и любое изменение требовало лезть в несколько несвязанных мест. Проект начал довольно быстро разрастаться.
Бо́льшая часть кода была переписана. В результате архитектура постепенно пришла к следующему конвейеру:
Scanner → ThreatObject → ThreatAnalyzer → ThreatDecision → ThreatHandler
Сканеры только находят объекты:
процессы
файлы
ключи реестра
задачи планировщика
службы
правила брандмауэра и так далее
ThreatObjects описывают найденный объект. Например
ProcessThreatObject
RegistryThreatObject
ThreatAnalyzers анализируют объект и выносят решение. Здесь находится основная логика определения подозрительности.
ProcessThreatAnalyzer
RegistryThreatAnalyzer
ThreatDecisions представляют единый формат результата анализа: какой объект найден, насколько он подозрителен и какое действие предлагается выполнить.
ThreatHandlers только принимает решение: удалить, отключить, переместить в карантин и так далее
Получилось разделение, которого очень не хватало первоначальному варианту. Сейчас в проекте 12 сканеров:
Процессы
файловая система (файлы/каталоги)
реестр
задачи планировщика
службы
WMI‑подписки
правила брандмауэра
Каталог автозагрузки
Профили пользователей
файл hosts
Сигнатурный сканер.
Сканер на руткит.
При этом сигнатурное сканирование — вспомогательная часть. Основу по‑прежнему составляют эвристики и анализ контекста объекта.
Отдельный UnlockObjectClass отвечает за снятие блокировок с файлов и каталогов. Он делает примерно то, что я вручную делал через свойства папки: получает владение объектом, убирает правила «Запретить» из DACL и сбрасывает атрибуты «скрытый» и «системный».
Карантин
Удалять найденное сразу — не очень хорошая идея. Подозрительные файлы, задачи планировщика и службы можно не удалять безвозвратно, а переместить в отдельное хранилище. В менеджере карантина видно, что туда было помещено. Объект можно восстановить или удалить окончательно. Для эвристического антималварного инструмента это изменение оказалось одним из самых важных. Чем больше проверок появляется, тем выше вероятность встретить что‑то нестандартное. Возможность откатить решение намного лучше безвозвратного удаления по одному подозрительному признаку.
Интерфейс и режимы
Первая версия была строго консольной и работала по большей части как скрипт. Сейчас у MinerSearch имеется:
Форма отчёта с итогами сканирования;
“Обзор угроз” (FormThreatReview), где для каждой угрозы можно выбрать действие и возможность применить одно решение ко всем объектам;
Настройки (FormSettings), в том числе автоматический приём решений для неизвестных угроз;
Несколько режимов запуска: консольный режим (без диалоговых окон), тихий, поддержка WinPE, сканирование выбранного каталога и режим только показа без лечения.
Весь код и релизы MinerSearch я выкладываю на Github
Немного скринов
Что бы я сделал иначе?
Если бы начинал сейчас, сразу бы закладывал разделение на сканеры и обработчики, как в текущей архитектуре, а не раздувал один большой метод. Рефакторинг занял очень много времени. Но тогда я вообще не думал про какую‑то там архитектуру, не было задачи изобрести новый антивирус. Была задача быстро решить проблему, вместо того, чтобы сносить винду и восстанавливать потерянные программы часами. Для небольшой программки это было удобно. Для программы, которая работает по эвристикам и потенциально может столкнуться с нестандартным ПО — это уже другой уровень.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.