ПЛУГИННЫЙ ВИРУС - ВЕРСИЯ 2.00 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1. Введение 2. Модульная структура в общем виде 3. Внешний контейнер 4. Плугины - на диске и в памяти 5. Формат плугина 6. Загрузчик 7. Взаимодействие плугинов 8. Взаимодействие плугинов через события 9. Приоритет вызовов событий 10. Взаимодействие плугинов через импорты/экспорты 11. Взаимодействие плугинов с загрузчиком (аттач/детач) 12. Оформление подпрограмм 13. Обработка ошибок (SEH) 14. Выбор числовых констант 15. Фича: FUCKAPI 1. ВВЕДЕНИЕ ~~~~~~~~~~~ Вот уже давно мысль о плугинных вирусах циркулирует в вирмэйкерских головах, не дает им покоя, иногда даже мешает спать. Сегодня это актуально, сегодня есть интернет и несметные полчища наших тайных обожателей - юзеров - раззявив ебала и занеся пакши над баттонами YES и OK, ждут, чтобы мы использовали их ресурсы. Почему плугины? Да, вирус, состоящий из отдельных модулей, действительно сложнее написать. Но модульная структура просто предназначена для обновления через интернет; и кроме того, возможно по-разному комбинируя плугины создавать функционально различные вирусы. Трудно предвидеть сейчас, какими возможностями мы захотим чтобы обладал вирус, после того, как он разойдется по тысячам компьютеров. А обновление одного плугина намного проще, чем обновление всего вируса. И наконец, это возможность дальнейшего развития, это повод для вирмэйкеров объединить усилия. На сегодня существует один реальный модульный вирус - это Hybris, и он показал себя отлично. Однако, моя субъективная оценка такова, что к хибрисовской системе плугинов трудно подключить что-нибудь свое, а построить на этой базе что-то действительно сложное, будет совсем не просто. Это не значит ничего плохого, хибрис по настоящему великолепен. Просто мне этого мало. Поэтому в то время, как в теплой бразилии рос хибрис, здесь, у нас, была предпринята попытка разработать свою собственную концепцию плугинного вируса, то есть вируса, по настоящему построенного на основе модулей: так, как я это вижу. Основное отличие заключается вот в чем. У хибриса это действительно ПЛУГИНЫ, то есть просто дополнительные фичи, прикручиваемые к вирусу. В проекте PGN2, плюс к тому что есть у хибриса, абсолютно ВЕСЬ вирус состоит из МОДУЛЕЙ, каждый из которых может быть обновлен. С другой стороны, хибрис направлен на интернет: большая часть его основных модулей ориетирована на распространение по сети. В проекте PGN2 распространение по интернету почти не поддерживается. То есть там все для этого есть, но самих плугинов для конкретно распространения - в опубликованных архивах - нет. Потому что PGN2 в первую очередь показывает реализацию модульной структуры, схему взаимодействия плугинов. Далее вам представляется краткое описание системы PGN2. 2. МОДУЛЬНАЯ СТРУКТУРА В ОБЩЕМ ВИДЕ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Вирус в зараженном файле: (местоположение расшифровщика и зашифрованных данных зависит от метода заражения) +---hostfile.exe/dll----+ | [...] | | [расшифровщик] | | [...] | | [зашифрованный вирус] | | [...] | +-----------------------+ Физическая (начальная) структура вируса, находящегося в зашифрованном файле, после расшифровки, выглядит так: +----------------------+ | [LDRWIN32.bin] | <-- загрузчик, для распаковки/запуска плугинов, | [compressed_plugin1] | необходим | [compressed_plugin2] | <-- запакованные плугины в формате PGN2, | [...] | присутствие опционально | [DD 0] | <-- завершающий DWORD=0, необходим загрузчику +----------------------+ 3. ВНЕШНИЙ КОНТЕЙНЕР ~~~~~~~~~~~~~~~~~~~~ Внешний контейнер: (наличие контейнера опционально) +----------------------+ | [compressed_plugin3] | <-- плугины в формате PGN2, | [compressed_plugin4] | присутствие опционально | [...] | | [DD 0] | <-- завершающий DWORD=0, необходим загрузчику +----------------------+ Внешний контейнер - это файл, в котором хранится часть вирусных плугинов. Ни один плугин (кроме загрузчика) с внешним контейнером непосредственно не работает. При аттаче свежевыкачанного плугина этот плугин автоматически (загрузчиком) добавляется во внешний контейнер. Когда стартует инфицированный файл, содержащий более новую версию некоторого плугина, этот плугин также будет добавлен в контейнер. Когда запускается файл, содержащий более старую версию некоторого плугина, или не содержащий его, этот плугин будет взят загрузчиком из контейнера. Таким образом, как только плугин принят из интернета и расшифрован, вызывается операция аттача плугина, и плугин 1. добавляется во внешний контейнер (возможно, заменяя старую версию) 2. подключается к работающей копии вируса и исполняется. При последующем же запуске файла со старой версией плугина, этот плугин будет заменен более новым, хранящимся в контейнере. Также, все в дальнейшем инфицируемые файлы будут содержать самые последние плугины. Единственно, пока еще не реализована возможность шифровки внешнего контейнера. 4. ПЛУГИНЫ - НА ДИСКЕ И В ПАМЯТИ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Для удобства работы с плугинами они организованы в список. Структура вируса в памяти: (список плугинов в памяти) LDRWIN32.PluginList DD ? <-- указатель на первую запись | +--------------+ --> физический образ (сжатый, формат PGN2) | list entry 1 | --> образ в памяти (исполняющийся PE EXE) +--------------+ | +--------------+ --> ... | list entry N | --> ... +--------------+ | NULL list_entry struc list_phys dd ? ; *pgn2_header, physical image list_virt dd ? ; *PE_in_memory, virtual image list_next dd ? ; next list entry or NULL ends Другими словами, на каждый плугин в памяти хранятся два образа: один - запакованный, то есть тот, который добавляется в каждый новый зараженный файл; а другой - распакованный в память PE EXE, который в настоящее время и исполняется. 5. ФОРМАТ ПЛУГИНА ~~~~~~~~~~~~~~~~~ Поскольку плугины представлены в формате PE EXE, писать их можно практически на чем угодно, подходящих компиляторов также полно, хотя мне вполне хватило tasm'а и borland c++. Единственно, откомпилированные PE-файлы должны удовлетворять следующим требованиям: 1. содержать фиксапы (если они используются в откомпилированном образе) 2. не содержать ресурсов (ресурсы будут поскипаны) После компилирования, с PE файлом следует сделать следующее: Тулза PE2PGN: 1. поскипать MZ-заголовок и DOS-овую часть 2. перерелоцировать на imagebase=0, physicaloffset=0 3. выкинуть ресурсы и фиксапы с типом 0 4. обнулить PE id, datetime, checksum, и прочие, в дальнейшем не используемые поля Тулза PACKER: 5. сжать файл Тулза HEADER: 6. добавить PGN2-заголовок (DWORD CRC32-id, DWORD version) Таким образом, физически плугины в формате PGN 2, а виртуально (в памяти) - в формате PE EXE. +---------------------+ | CRC32-id | ; crc32('имя плугина') +---------------------+ | version | ; версия +---------------------+ | compressed_size | ; длина запакованных данных (Z_CODING) +---------------------+ | decompressed_size | ; длинна распакованных данных +---------------------+ | запакованный PE EXE | ; длина = compressed_size | ... | +---------------------+ pgn2_header struc pgn2_id dd ? ; CRC32('lowercased name') pgn2_version dd ? ; 1,2,... >=100000--not-in-file pgn2_compressed dd ? ; compressed data size pgn2_decompressed dd ? ; decompressed data size, PE format ; BYTE * compressed_size ends DWORD CRC32-id, который идет до запакованного тела плугина - это CRC32 от имени плугина маленькими буквами. DWORD version - это версия плугина, которая если >= 100000, то такой плугин не будет добавляться в новоинфицируемые файлы. Это значит, что такой плугин будет храниться только во внешнем контейнере, и подгружаться оттуда при каждом запуске, но никогда не будет включен в зараженный файл. Это фича используется тогда, когда новые версии этого плугина планируется постоянно выкачивать из инета. В плане загрузки плугинов в память (а загружает их туда наш собственный загрузчик), существуют следующие отличия: - imagebase больше не должен быть выровнен, нам это пофигу, так как в ring3: выделение памяти через GlobalAlloc, align=DWORD в ring0: выделение памяти через PageAllocate, align=4K - большинство записей в PE-заголовке нулевые, т.е. не используются - аттрибуты и имена секций не имеют смысла (вся память r/w), нам нужны только оффсеты и длины - при импорте функций из других плугинов - имена плугинов начинаются на @ - возможны любые импорты, экспорты и фиксапы, также возможны экспорты из системных DLL'ек по именам - не поддерживаются ресурсы - точка входа (если есть) вызывается первой - затем вызывается начальное событие - возможно наличие экспортируемой подпрограммы unload() - подпрограммы интерплугинного взаимодействия: экспортируемая HandleEvent() и импортируемая Event() (опционально) 6. ЗАГРУЗЧИК ~~~~~~~~~~~~ LDRWIN32 - это специальный плугин, содержащий в себе блок кода LDRWIN32.bin, причем каждый из них называется загрузчиком. LDRWIN32.bin - это блок кода, который просто содержится внутри соответствующего плугина, и выполняет следующие функции: если к этому блоку кода приписать пачку плугинов (запакованных, в формате PGN2), и передать ему управление, то он построит в памяти список плугинов, распакует туда PE EXE-образы и запустит вирус. Более точно, задача загрузчика такая: - взять все имеющиеся плугины; - загрузить дополнительные плугины из внешнего контейнера; - выбрать новейшие; - построить список плугинов; - распаковать плугины; - загрузить их в память как PE-файлы; - настроить фиксапы, ипморты и экспорты; - вызвать все точки входа (для каждого плугина); - послать плугинам начальное событие; - если событие не обработано, освободить память; - вернуться в программу-носитель. Также загрузчик содержит подпрограмму LDRWIN32.ldrwin32_copy(), которая вызывается для построения новой копии вируса (нового списка плугинов). Эта подпрограмма использует текущий список плугинов как родительский, и без чтения внешнего контейнера, просто выделяет память под копию списка и образы плугинов и копирует их туда, после чего генерит соответствующее событие. В настоящее время эта фича используется (под win9x) для построения второй активной копии вируса в ring-0. Кроме того, плугин LDRWIN32 содержит следующие public-функции и переменные: PluginList - указатель на первую запись списка плугинов. (см.PGN2.INC) list_entry struc list_phys dd ? ; *pgn2_header, physical image list_virt dd ? ; *PE_in_memory, virtual image list_next dd ? ; next list entry or NULL ends Чтобы получить значение импортируемого DWORD'а, надо сделать так: extrn TestDword:DWORD ; make imported entry mov eax, TestDword + 2 ; FF 25 xx xx xx xx: JMP DWORD PTR [xxxxxxxx] mov eax, [eax] ; = address mov eax, [eax] ; = value Вместо этого можно ипортировать функцию LDRWIN32.GetPluginList(), которая вернет значение PluginList в EAX. 7. ВЗАИМОДЕЙСТВИЕ ПЛУГИНОВ ~~~~~~~~~~~~~~~~~~~~~~~~~~ Реализованы две взаимодополняющих схемы взаимодействия плугинов: через внутренние события и через импортируемые/экспортируемые функции. Отличие схем в том, что при взаимодействии через события, плугин(ы), принимающие события могут как присутствовать так и нет; но если некий плугин A импортирует функцию из плугина B, то плугин B присутствовать обязан, причем эта функция должна быть описана в его экспортах; в противном случае плугин А будет отключен. Таким образом взаимодействие через импорты/экспорты обеспечивает доступ к основным, общим функциям, типа работы с памятью и файлами, а событиями обеспечивается подключение плугинов "на будущее". 8. ВЗАИМОДЕЙСТВИЕ ПЛУГИНОВ ЧЕРЕЗ СОБЫТИЯ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Под событиями имеется в виду внутренняя модель событий, но, конечно же, никак не маздайные события. Хотя и на них тоже можно бы было построить взаимодействие плугинов. Общение между плугинами происходит так: Для того, чтобы принимать события из других плугинов, плугин экспортирует функцию HandleEvent(). Для того, чтобы посылать события, плугин импортирует функцию Event() из загрузчика LDRWIN32. У каждого события есть уникальный номер и один юзерский параметр. Для вызова события, делаем так: ... extern Event:PROC ; объявляем импортируемую процедуру, из LDRWIN32 ... push push call Event ; вызываем загрузчик, add esp, 8 ; он распределяет событие по всем плугинам ... or eax, eax jnz event_handled event_not_handled: ... или, на C++ ... int __cdecl Event(DWORD EventID, DWORD UserParam); ... if ( Event(, ) ) { ... // event handled } ... Подпрограмма LDRWIN32.Event просто распределяет событие по тем плугинам, у которых есть public-подпрограмма HandleEvent. Так что, для обработки событий, делаем так: ... public HandleEvent ; объявляем экспортируемую подпрограмму ... HandleEvent: mov eax, [esp+4] ; event_id mov ecx, [esp+8] ; user_param ... mov eax, 0/1/-1 ; return value retn или, на C++ int __export __cdecl HandleEvent(DWORD EventID, DWORD UserParam) { if (EventID == ) { ... return 1/-1; } return 0; } Как видно, подпрограмма Event возвращает результат в EAX: 0 если событие не было обработано -1 если событие было обработано с результатом -1 (остановить распределение событий) N если N плугинов обработали событие с результатом 1 Очевидно, что после того, как некоторое событие обрабатывается (подпрограммой HandleEvent) с результатом -1, загрузчик просто останавливает распределение событий и возвращает -1 как результат. В остальных случаях LDRWIN32.Event просто суммирует результаты от HandleEvent()'ов (нули и единицы) и возвращает сумму. 9. ПРИОРИТЕТ ВЫЗОВОВ СОБЫТИЙ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Предположим, что несколько плугинов висят на одном и том же событии, то есть ждут его, и затем производят какие-то действия. Как выяснить, в каком порядке они (плугины) должны вызываться при распределении событий? Для этого у каждого плугина есть параметр PRIORITY, от 0 до 10 включительно, в соответствии с которым загрузчик решает, какой плугин вызовется раньше, а какой позже. По умолчанию значение PRIORITY должно быть равно 5, и изменять его не рекомендуется. Если вы пишете плугин A, который должен (при одном и том же событии) быть вызван раньше плугина B, то поставьте для A значение PRIORITY на 1 меньше. 10. ВЗАИМОДЕЙСТВИЕ ПЛУГИНОВ ЧЕРЕЗ ИМПОРТЫ/ЭКСПОРТЫ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ В дополнение к функциям HandleEvent() и Event(), которые суть основной способ коммуникации между плугинами, плугины могут использовать импорты и экспорты друг между другом, и иморты из системных DLL'ек по именам. Единственное отличие от импортов из системных DLL'ек в том, что имена импортируемых плугинов в import table должны начинаться на @. Пример .DEF-файла, передаваемого TLINK32'у при линковке плугина: EXPORTS HandleEvent IMPORTS Event = @LDRWIN32.Event fuckit = KERNEL32.DeleteFileA ... 11. ВЗАИМОДЕЙСТВИЕ ПЛУГИНОВ С ЗАГРУЗЧИКОМ (АТТАЧ/ДЕТАЧ) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ У плугинов может быть ненулевая точка входа (EntryPointRVA), которая вызывается сразу после того, как плугин будет загружен в память, но до посылки каких-либо событий. Во время этого вызова также возможно вызывать события. У процедуры EntryPoint() один DWORD-параметр, который обычно равен нулю. Но может также содержать и некоторое другое значение - код возврата из процедуры unload(). А эта процедура, если она в плугине есть, вызывается до того, как плугин будет выгружен из памяти, в случае апдейта плугина более новой версией, либо в случае просто выгрузки плугина из памяти. Если это выгрузка из памяти, то unload() должна освободить всю выделенную память и убить все созданные нити. Если это апдейт, то есть возможность передать некоторые данные из старой версии плугина в новую. void __export __cdecl EntryPoint(DWORD oldver_unload_code) { if (oldver_unload_code == 0) { ... ; просто загрузка в память } else { ... ; замена старой версии в рантайме, при этом oldver_unload_code ; может быть указателем на некоторые данные } } //EntryPoint int __export __cdecl unload(int why) { if (why==UT_UNINSTALL) { ... ; выгрузка из памяти return 0; } if (why==UT_UPDATE) { ... ; апдейт, т.е. выгрузка, с тем чтобы заменить новой версией return 1; } } //unload Следующие две public-подпрограммы, существующие в загрузчике, позволяют производить аттач и детач плугинов в рантайме. 1. int __cdecl ldrwin32_attach(BYTE* buf, DWORD* bufsize) Приаттачить пачку плугинов. Буфер - набор запакованных плугинов, желательно чтобы все заканчивалось на 'DD 0'. То есть формат буфера такой же, как и у внешнего контейнера. Длина буфера используется на случай, когда не найден завершающий 'DD 0'. Если один из этих новопришедших плугинов у нас уже есть, то в загрузчике происходит следующее: - проверить, если пришедший плугин новый; обновить внешний контейнер - старые плугины: вызвать unload(UT_UPDATE), запомнить exitcode (exitcode может быть указателем на динамически-аллоцируемый блок) - старые плугины: освободить память - новые плугины: распаковать, загрузить в память, настроить импорты, экспорты, фиксапы - заново проверить наличие всех наобходимых экспортов между всеми плугинами, и пометить плугины, которым не хватает экспортов, как UNRESOLVED (и в дальнейшем они работать не будут) - новые плугины: вызвать точки входа, передавая exitcodе от соответствующих unload()'ов как параметр После аттача, генерится евент EV_LDRWIN32_ATTACHED 2. void __cdecl ldrwin32_detach_me() Эта продпрограмма вызывается, когда какой-то плугин хочет себя отгрузить. Вызывающий плугин в таком случае будет ЗАМЕНЕН фэйковым (нулевым) плугином с версией на 1 больше, но с тем же ID, и с нулевыми compressed/decompressed size. Этот новый нулевой плугин будет сохранен во внешний контейнер, в дальнейшем по возможности замещая старую версию, которая захотела себя детачить. Детач используется когда некий плугин считает, что выполнил свою миссию, и больше не должен на этой машиние исполняться никогда. После детача генерится евент EV_LDRWIN32_DETACHED. 12. ОФОРМЛЕНИЕ ПОДПРОГРАММ ~~~~~~~~~~~~~~~~~~~~~~~~~~ Желательно, чтобы все public-подпрограммы были написаны согласно с cdecl конвенцией, то есть: - порядок PUSHа параметров на стэк ОБРАТНЫЙ, т.е. после CALL'а, первый (последний запушеный) параметр находится по адресу [ESP+4], 2й по [ESP+8]. - выход из подпрограмм по RETN (0xC3) - после CALL'а вызывающий делает - подпрограммы могут изменять EAX,ECX,EDX - подпрограммы не могут изменять EBX,ESI,EDI,EBP - функции возвращают результат в EAX 13. ОБРАБОТКА ОШИБОК (SEH) ~~~~~~~~~~~~~~~~~~~~~~~~~~ 1. SEH'ом ошибки обрабатываются только во время вызовов EVENT'ов. Когда вызывается EntryPoint или unload(), никакого SEH'а, кроме общего, нет. Когда обрабатывается возникшая ошибка, загрузчик LDRWIN32 ищет адрес ошибки, затем по этому адресу - соответствующий плугин, и отключает этот плугин. (ставит флаг FL_PGN2_SEHERROR) После этого происходит операция обновления межплугинных импортов/экспортов, и все плугины, директом (не через события) вызывающие глючный, будут также отключены. (UNRESOLVED) 2. Так как система PGN2 была разработана под win32/ring-3, т.е. win9x/ring0-код возможен, но не желателен, то логика работы не должна основываться на ring-0. Весь ring0-код должен быть по возможности коротким и независимым, дабы не сглючило. 14. ВЫБОР ЧИСЛОВЫХ КОНСТАНТ ~~~~~~~~~~~~~~~~~~~~~~~~~~~ В случае, если вы пишете плугин, который должен будет посылать события другим плугинам, для этих событий придется выбрать уникальные номера. Я предлагаю сделать это так: к имени плугина (маленькими буквами) припишите свой никнэйм и посчитайте от этого дела CRC32 (прилагается тулза CRC32). Затем прибавляя к полученному числу 0,1,2 и т.д. получите уникальные номера для ваших событий. Сделано это исключительно для того, чтобы номера событий не пересекались, в том фантастическом случае, если вы захотите поддержать проект парой своих собственных плугинов. 15. Фича: FUCKAPI ~~~~~~~~~~~~~~~~~ Поскольку часть интерплугинного взаимодействия возлагается на импорты/экспорты, то плугины будут содержать имена своих public-подпрограмм, что хорошо для касперского, а значит плохо для нас. Поэтому была придумана фича, кояя сделает изучение бинарной версии вируса незабываемой. Итак, имена всех плугинов и public-процедур во всех плугинах, обрабатываются следующим образом: 1. первый символ @ пропускается (если он есть) 2. от имени считается crc32 3. этим crc32 инициализируется randseed, после чего генерируется строка из псевдослучайных символов (1..255), в ту же длину Естественно, что это справедливо только для интерплугинных public-процедур, а при импорте из kernel'а имена останутся без изменений. * * *