Вот уже давно мысль о плугинных вирусах циркулирует в вирмэйкерских головах, не дает им покоя, иногда даже мешает спать. Сегодня это актуально, сегодня есть интернет и несметные полчища наших тайных обожателей - юзеров - раззявив ебала и занеся пакши над баттонами YES и OK, ждут, чтобы мы использовали их ресурсы.
Почему плугины?
Да, вирус, состоящий из отдельных модулей, действительно сложнее написать. Но модульная структура просто предназначена для обновления через интернет; и кроме того, возможно по-разному комбинируя плугины создавать функционально различные вирусы. Трудно предвидеть сейчас, какими возможностями мы захотим чтобы обладал вирус, после того, как он разойдется по тысячам компьютеров. А обновление одного плугина намного проще, чем обновление всего вируса. И наконец, это возможность дальнейшего развития, это повод для вирмэйкеров объединить усилия.
На сегодня существует один реальный модульный вирус - это Hybris, и он показал себя отлично. Однако, моя субъективная оценка такова, что к хибрисовской системе плугинов трудно подключить что-нибудь свое, а построить на этой базе что-то действительно сложное, будет совсем не просто. Это не значит ничего плохого, хибрис по настоящему великолепен.
Просто мне этого мало.
Поэтому в то время, как в теплой бразилии рос хибрис, здесь, у нас, была предпринята попытка разработать свою собственную концепцию плугинного вируса, то есть вируса, по настоящему построенного на основе модулей: так, как я это вижу.
Основное отличие заключается вот в чем. У хибриса это действительно ПЛУГИНЫ, то есть просто дополнительные фичи, прикручиваемые к вирусу. В проекте PGN2, плюс к тому что есть у хибриса, абсолютно ВЕСЬ вирус состоит из МОДУЛЕЙ, каждый из которых может быть обновлен.
С другой стороны, хибрис направлен на интернет: большая часть его основных модулей ориетирована на распространение по сети. В проекте PGN2 распространение по интернету почти не поддерживается. То есть там все для этого есть, но самих плугинов для конкретно распространения - в опубликованных архивах - нет. Потому что PGN2 в первую очередь показывает реализацию модульной структуры, схему взаимодействия плугинов.
Далее вам представляется краткое описание системы PGN2.
Вирус в зараженном файле: (местоположение расшифровщика и зашифрованных данных зависит от метода заражения)
+---hostfile.exe/dll----+ | [...] | | [расшифровщик] | | [...] | | [зашифрованный вирус] | | [...] | +-----------------------+
Физическая (начальная) структура вируса, находящегося в зашифрованном файле, после расшифровки, выглядит так:
+----------------------+ | [LDRWIN32.bin] | <-- загрузчик, для распаковки/запуска плугинов, | [compressed_plugin1] | необходим | [compressed_plugin2] | <-- запакованные плугины в формате PGN2, | [...] | присутствие опционально | [DD 0] | <-- завершающий DWORD=0, необходим загрузчику +----------------------+
Внешний контейнер: (наличие контейнера опционально)
+----------------------+ | [compressed_plugin3] | <-- плугины в формате PGN2, | [compressed_plugin4] | присутствие опционально | [...] | | [DD 0] | <-- завершающий DWORD=0, необходим загрузчику +----------------------+
Внешний контейнер - это файл, в котором хранится часть вирусных плугинов. Ни один плугин (кроме загрузчика) с внешним контейнером непосредственно не работает. При аттаче свежевыкачанного плугина этот плугин автоматически (загрузчиком) добавляется во внешний контейнер. Когда стартует инфицированный файл, содержащий более новую версию некоторого плугина, этот плугин также будет добавлен в контейнер. Когда запускается файл, содержащий более старую версию некоторого плугина, или не содержащий его, этот плугин будет взят загрузчиком из контейнера.
Таким образом, как только плугин принят из интернета и расшифрован, вызывается операция аттача плугина, и плугин
При последующем же запуске файла со старой версией плугина, этот плугин будет заменен более новым, хранящимся в контейнере. Также, все в дальнейшем инфицируемые файлы будут содержать самые последние плугины.
Единственно, пока еще не реализована возможность шифровки внешнего контейнера.
Для удобства работы с плугинами они организованы в список. Структура вируса в памяти: (список плугинов в памяти)
LDRWIN32.PluginList DD ? <-- указатель на первую запись
|
+--------------+ --> физический образ (сжатый, формат PGN2)
| list entry 1 | --> образ в памяти (исполняющийся PE EXE)
+--------------+
|
+--------------+ --> ...
| list entry N | --> ...
+--------------+
|
NULL
Другими словами, на каждый плугин в памяти хранятся два образа: один - запакованный, то есть тот, который добавляется в каждый новый зараженный файл; а другой - распакованный в память PE EXE, который в настоящее время и исполняется.
Поскольку плугины представлены в формате PE EXE, писать их можно практически на чем угодно, подходящих компиляторов также полно, хотя мне вполне хватило tasm'а и borland c++.
Единственно, откомпилированные PE-файлы должны удовлетворять следующим требованиям:
После компилирования, с PE файлом следует сделать следующее:
Таким образом, физически плугины в формате PGN 2, а виртуально (в памяти) - в формате PE EXE.
+---------------------+
| CRC32-id | ; crc32('имя плугина')
+---------------------+
| version | ; версия
+---------------------+
| compressed_size | ; длина запакованных данных (Z_CODING)
+---------------------+
| decompressed_size | ; длинна распакованных данных
+---------------------+
| запакованный PE EXE | ; длина = compressed_size
| ... |
+---------------------+
DWORD CRC32-id, который идет до запакованного тела плугина - это CRC32 от имени плугина маленькими буквами.
DWORD version - это версия плугина, которая если >= 100000, то такой плугин не будет добавляться в новоинфицируемые файлы. Это значит, что такой плугин будет храниться только во внешнем контейнере, и подгружаться оттуда при каждом запуске, но никогда не будет включен в зараженный файл. Это фича используется тогда, когда новые версии этого плугина планируется постоянно выкачивать из инета.
В плане загрузки плугинов в память (а загружает их туда наш собственный загрузчик), существуют следующие отличия:
LDRWIN32 - это специальный плугин, содержащий в себе блок кода LDRWIN32.bin, причем каждый из них называется загрузчиком. LDRWIN32.bin - это блок кода, который просто содержится внутри соответствующего плугина, и выполняет следующие функции: если к этому блоку кода приписать пачку плугинов (запакованных, в формате PGN2), и передать ему управление, то он построит в памяти список плугинов, распакует туда PE EXE-образы и запустит вирус.
Более точно, задача загрузчика такая:
Также загрузчик содержит подпрограмму LDRWIN32.ldrwin32_copy(), которая вызывается для построения новой копии вируса (нового списка плугинов). Эта подпрограмма использует текущий список плугинов как родительский, и без чтения внешнего контейнера, просто выделяет память под копию списка и образы плугинов и копирует их туда, после чего генерит соответствующее событие. В настоящее время эта фича используется (под win9x) для построения второй активной копии вируса в ring-0.
Кроме того, плугин LDRWIN32 содержит следующие public-функции и переменные:
PluginList - указатель на первую запись списка плугинов. (см.PGN2.INC)
Чтобы получить значение импортируемого DWORD'а, надо сделать так:
Вместо этого можно ипортировать функцию LDRWIN32.GetPluginList(), которая вернет значение PluginList в EAX.
Реализованы две взаимодополняющих схемы взаимодействия плугинов: через внутренние события и через импортируемые/экспортируемые функции.
Отличие схем в том, что при взаимодействии через события, плугин(ы), принимающие события могут как присутствовать так и нет; но если некий плугин A импортирует функцию из плугина B, то плугин B присутствовать обязан, причем эта функция должна быть описана в его экспортах; в противном случае плугин А будет отключен.
Таким образом взаимодействие через импорты/экспорты обеспечивает доступ к основным, общим функциям, типа работы с памятью и файлами, а событиями обеспечивается подключение плугинов "на будущее".
Под событиями имеется в виду внутренняя модель событий, но, конечно же, никак не маздайные события. Хотя и на них тоже можно бы было построить взаимодействие плугинов.
Общение между плугинами происходит так:
Для того, чтобы принимать события из других плугинов, плугин экспортирует функцию HandleEvent().
Для того, чтобы посылать события, плугин импортирует функцию Event() из загрузчика LDRWIN32.
У каждого события есть уникальный номер и один юзерский параметр.
Для вызова события, делаем так:
или, на C++
Подпрограмма LDRWIN32.Event просто распределяет событие по тем плугинам, у которых есть public-подпрограмма HandleEvent.
Так что, для обработки событий, делаем так:
или, на C++
Как видно, подпрограмма Event возвращает результат в EAX:
Очевидно, что после того, как некоторое событие обрабатывается (подпрограммой HandleEvent) с результатом -1, загрузчик просто останавливает распределение событий и возвращает -1 как результат. В остальных случаях LDRWIN32.Event просто суммирует результаты от HandleEvent()'ов (нули и единицы) и возвращает сумму.
Предположим, что несколько плугинов висят на одном и том же событии, то есть ждут его, и затем производят какие-то действия.
Как выяснить, в каком порядке они (плугины) должны вызываться при распределении событий?
Для этого у каждого плугина есть параметр PRIORITY, от 0 до 10 включительно, в соответствии с которым загрузчик решает, какой плугин вызовется раньше, а какой позже.
По умолчанию значение PRIORITY должно быть равно 5, и изменять его не рекомендуется.
Если вы пишете плугин A, который должен (при одном и том же событии) быть вызван раньше плугина B, то поставьте для A значение PRIORITY на 1 меньше.
В дополнение к функциям HandleEvent() и Event(), которые суть основной способ коммуникации между плугинами, плугины могут использовать импорты и экспорты друг между другом, и иморты из системных DLL'ек по именам.
Единственное отличие от импортов из системных DLL'ек в том, что имена импортируемых плугинов в import table должны начинаться на @.
Пример .DEF-файла, передаваемого TLINK32'у при линковке плугина:
У плугинов может быть ненулевая точка входа (EntryPointRVA), которая вызывается сразу после того, как плугин будет загружен в память, но до посылки каких-либо событий. Во время этого вызова также возможно вызывать события. У процедуры EntryPoint() один DWORD-параметр, который обычно равен нулю. Но может также содержать и некоторое другое значение - код возврата из процедуры unload(). А эта процедура, если она в плугине есть, вызывается до того, как плугин будет выгружен из памяти, в случае апдейта плугина более новой версией, либо в случае просто выгрузки плугина из памяти. Если это выгрузка из памяти, то unload() должна освободить всю выделенную память и убить все созданные нити. Если это апдейт, то есть возможность передать некоторые данные из старой версии плугина в новую.
Следующие две public-подпрограммы, существующие в загрузчике, позволяют производить аттач и детач плугинов в рантайме.
int __cdecl ldrwin32_attach(BYTE* buf, DWORD* bufsize)
Приаттачить пачку плугинов. Буфер - набор запакованных плугинов, желательно чтобы все заканчивалось на 'DD 0'. То есть формат буфера такой же, как и у внешнего контейнера. Длина буфера используется на случай, когда не найден завершающий 'DD 0'.
Если один из этих новопришедших плугинов у нас уже есть, то в загрузчике происходит следующее:
После аттача, генерится евент EV_LDRWIN32_ATTACHED
void __cdecl ldrwin32_detach_me()
Эта продпрограмма вызывается, когда какой-то плугин хочет себя отгрузить.
Вызывающий плугин в таком случае будет ЗАМЕНЕН фэйковым (нулевым) плугином с версией на 1 больше, но с тем же ID, и с нулевыми compressed/decompressed size.
Этот новый нулевой плугин будет сохранен во внешний контейнер, в дальнейшем по возможности замещая старую версию, которая захотела себя детачить.
Детач используется когда некий плугин считает, что выполнил свою миссию, и больше не должен на этой машиние исполняться никогда.
После детача генерится евент EV_LDRWIN32_DETACHED.
Желательно, чтобы все public-подпрограммы были написаны согласно с cdecl конвенцией, то есть:
Когда обрабатывается возникшая ошибка, загрузчик LDRWIN32 ищет адрес ошибки, затем по этому адресу - соответствующий плугин, и отключает этот плугин. (ставит флаг FL_PGN2_SEHERROR)
После этого происходит операция обновления межплугинных импортов/экспортов, и все плугины, директом (не через события) вызывающие глючный, будут также отключены. (UNRESOLVED)
Весь ring0-код должен быть по возможности коротким и независимым, дабы не сглючило.
В случае, если вы пишете плугин, который должен будет посылать события другим плугинам, для этих событий придется выбрать уникальные номера. Я предлагаю сделать это так: к имени плугина (маленькими буквами) припишите свой никнэйм и посчитайте от этого дела CRC32 (прилагается тулза CRC32). Затем прибавляя к полученному числу 0,1,2 и т.д. получите уникальные номера для ваших событий. Сделано это исключительно для того, чтобы номера событий не пересекались, в том фантастическом случае, если вы захотите поддержать проект парой своих собственных плугинов.
Поскольку часть интерплугинного взаимодействия возлагается на импорты/экспорты, то плугины будут содержать имена своих public-подпрограмм, что хорошо для касперского, а значит плохо для нас.
Поэтому была придумана фича, кояя сделает изучение бинарной версии вируса незабываемой.
Итак, имена всех плугинов и public-процедур во всех плугинах, обрабатываются следующим образом:
Естественно, что это справедливо только для интерплугинных public-процедур, а при импорте из kernel'а имена останутся без изменений.
[Вернуться к списку] [Комментарии (0)]