Maximize
Bookmark

VX Heaven

Library Collection Sources Engines Constructors Simulators Utilities Links Forum

Плугинный вирус - версия 2.00

Z0mbie

[Вернуться к списку] [Комментарии]
  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:
  1. сжать файл
Тулза HEADER:
  1. добавить 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, то такой плугин не будет добавляться в новоинфицируемые файлы. Это значит, что такой плугин будет храниться только во внешнем контейнере, и подгружаться оттуда при каждом запуске, но никогда не будет включен в зараженный файл. Это фича используется тогда, когда новые версии этого плугина планируется постоянно выкачивать из инета.

В плане загрузки плугинов в память (а загружает их туда наш собственный загрузчик), существуют следующие отличия:

6. Загрузчик

LDRWIN32 - это специальный плугин, содержащий в себе блок кода LDRWIN32.bin, причем каждый из них называется загрузчиком. LDRWIN32.bin - это блок кода, который просто содержится внутри соответствующего плугина, и выполняет следующие функции: если к этому блоку кода приписать пачку плугинов (запакованных, в формате PGN2), и передать ему управление, то он построит в памяти список плугинов, распакует туда PE EXE-образы и запустит вирус.

Более точно, задача загрузчика такая:

Также загрузчик содержит подпрограмму 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    <user_param>
        push    <event_id>
        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_id>, <user_param>) )
        {
                ... // 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 == <some_event_id>)
                {
                        ...
                        return 1/-1;
                }
                return 0;
        }
 

Как видно, подпрограмма Event возвращает результат в EAX:

Очевидно, что после того, как некоторое событие обрабатывается (подпрограммой 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 конвенцией, то есть:

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'а имена останутся без изменений.

[Вернуться к списку] [Комментарии]
By accessing, viewing, downloading or otherwise using this content you agree to be bound by the Terms of Use! vxer.org aka vx.netlux.org
deenesitfrplruua