Настольная книга компьютерного исследователя


Вирусные алгоритмы: обратное проектирование (часть 2)

Практика

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

- проверяем PE файл на валидность (наличие таблицы фиксапов и т.п.)
- выделяем память под виртуальный образ файла в памяти и под флаги на каждый байт образа файла
- загружаем по виртуальным адресам:
1. досовскую часть и PE заголовок файла
2. секции файла
- разбираем PE заголовок, анализируем все его указатели и длины, помечаем начала и концы секций как метки и т.п.
- разбираем импорты
- разбираем экспорты
- разбираем фиксапы
- разбираем ресурсы
- ищем в файле начала процедур по сигнатурам (типа push ebp/mov ebp,esp) и помечаем их для-последующего-анализа
- помечаем точку входа
- дизассемблируем файл, алгоритм описан в статье «о пермутации»; однако, есть пара отличий: когда кончаются все помеченные-для—последующего-анализа метки, мы ищем метки-на-которые-ссылаются-указатели, и проверяем, не являются ли они кодом. (см. ниже)
- создаем список из опкодов, меток, указателей и т.п. заметим, что в этом списке уже будут объекты нулевой длины (метки), то есть происходит переход к более высокому уровню абстракции; по сути этот список — своеобразное представление исходника. после перехода от линейных массивов к списку, массивы больше не нужны.
- изменяем список;
во время отладки я вставлял NOP’ы между всеми инструкциями файла
- вычисляем новые виртуальные адреса для всех записей списка
- пересчитываем таблицу фиксапов
- пересчитываем значения указателей (т.е. rva, фиксапов и относительных смещений условных переходов);
если некоторые условные переходы пришлось увеличить, то повторяем все с пересчета виртуальных адресов
- ассемблируем список (собираем все данные в один массив)
- записываем файл на диск
- дописываем к концу файла оверлей, если он был
- пересчитываем контрольную сумму, если она была ненулевая

Что означают фразы типа «разбираем PE заголовок» / «разбираем импорты»? Это значит, что мы выделяем среди них метки, указатели и другие специальные объекты и выставляем для них во флагах соответствующие биты. Например, байт по адресу pe_header+28h (28h=EntryPointRva) поимеет флаги типа FLAG_DWORD и FLAG_RVA, а тот адрес, на который он показывает, будет обозначен как FLAG_LABEL и FLAG_CREF.

Какие специальные объекты будут присутствовать в списке?

  1. Метки, то есть то, на что ссылаются RVA и FIXUP’ы.
  2. RVA, то есть дворд, который указывает на метку.
  3. FIXUP, то есть дворд, такой же как RVA, но + IMAGEBASE, притом адрес должен быть занесен в таблицу фиксапов.
  4. так называемая DELTA, то есть разница между адресами двух меток.
  5. инструкция
  6. блок данных

Теперь о том, как мы отличаем код от данных. Напомню, что мы рассматриваем адрес на который есть ссылки только через указатели (rva и fixup’ы), но нет ссылок через call/jmp. Итак, берем предполагаемую процедуру и разбираем ее по одной инструкции. Для каждой инструкции проверяем, не является ли она глючной, типа 00 00, FF FF, F4 (hlt), CD (int), и т.п., то есть таким опкодом, который в процедурах PE файлов не встречается. Если какой-либо из байтов предполагаемой инструкции содержит флаги типа «метка» или «данные», то это не процедура. Если инструкция имеет относительный адрес (jmp,call,jxx,jecxz,…), то он должен  показывать на что-нибудь приличное, то есть не в блок данных и не в середину другой инструкции. Повторяются же такие проверки до тех пор, пока не будет найден RET или JMP.

Однако, есть и такие ситуации, когда отличить код от данных достаточно сложно. Например, такой объект, как метка (label), вроде бы не может присутствовать в середине инструкции, сгенеренной hll компилятором. Но вот — нихрена подобного. Очень даже может. Рассмотрим типичный случай.

avpbase.dll:
100050D3 83E904 sub ecx, 4
100050D6 720C jb 100050E4
100050D8 83E003 and eax, 3
100050DB 03C8 add ecx,eax
100050DD FF2485F0500010 jmp dword ptr [100050F0+eax*4] (1)
100050E4 FF248DE8510010 jmp dword ptr [100051E8+ecx*4]
100050EB 90 nop
100050EC FF248D6C510010 jmp dword ptr [1000516C+ecx*4] (2)
100050F3 90 nop
100050F4 00510010 dd 10005100
100050F8 2C510010 dd 1000512C

Как видно, адрес 100050F0 находится в середине инструкции (2), и в то же время используется в инструкции (1). Почему так происходит, догадаться несложно. В результате, о инструкции (2) нельзя с полной уверенностью сказать, является ли она кодом или данными. То есть, автоматически нельзя определить, было ли в исходнике написано 100050F4 — 4 или 100050EC + 4. Короче говоря, нет возможности пофиксить такой поинтер, и файл придется оставить в покое.

Или вот, замечательный пример от дяди Рошаля.

FAR.EXE:
004474D8 B8E1C24200 mov eax,0042C2E1 ; ==42C350-6Fh ; (1)
004474DD 6A00 push 00
004474DF 6800000100 push 00010000
004474E4 83C06F add eax, 6F ; ==111
004474E7 50 push eax
004474E8 E8AF180100 call 00458D9C

0042C2DB E8B0450200 call 00450890
0042C2E0 83C408 add esp, 08 ; (2)
0042C2E3 8D9500FFFFFF lea edx,[ebp][0FFFFFF00]

0042C350 55 push ebp
0042C351 8BEC mov ebp, esp
0042C353 833D5054460003 cmp d,[000465450], 03

И, в дополнение ко всему прочему, бывает еще одна хуевая фишка. Это куски 16-битного кода в 32-битных приложениях, типа антивирусов и форматеров. Со всеми проистекающими отсюда глюками.

Движок

Движок написан на борман C++, правда без классов и прочих фич. Основная функция engine() с хуевой кучей параметров, идет, как и следует, самой первой, а после нее есть еще несколько внутренних подпрограммок. Все это дело называется kernel (не путать с маздайным) и находится в файлах engine.cpp & .hpp; соответственно код и константы. Из кернела вызывается так называемый внешний юзерский мутатор (mutate.cpp), коий может и должен быть модифицирован пользователем. Мутатор, то есть, собственно инфектор, оперирует исключительно со списком из структур, которые будем называть хуйнями (hooy), ибо мозги уже отказывают. В хуйнях могут быть метки, опкоды, блоки данных, словом все то, о чем говорилось в начале.

В результате, простейший метод заражения файла таков:

1. Найти две любые подряд идущие инструкции; после первой инструкции вставить JMP на вторую, а после JMP’а — зашифрованое тело вируса.
2. Аналогично для декриптора.
3. Аналогично для команды вызова декриптора.

Причем, это только один из вариантов. А вариантов этих должно быть очень много (выбираться будет рандомный).

Применение в вирусах

Прежде всего, движок жрет не просто много, а очень много памяти. При этом, может получиться глюк на кривом файле; да и на нормальном тоже. Аллокация памяти предоставляется движку пользователем. Перед вызовом движка выделяйте дохуя памяти, мегабайта 32. Стратегия аллокации весьма специфическая: движок будет только запрашивать память, и никогда — отдавать. Однако для каждого файла используется одна и та же память, т.е.
после возврата из движка все количество выделенной памяти нужно сбросить в 0. То есть произвести такой глобальный release. Реально движок использует (17*SizeOfImage) байт на временные линейные массивы, плюс байт по 40 на каждую инструкцию/запись списка. Код движка пермутируемый, то есть согласуется с уже описанными правилами в тексте «требования к движкам». Движок мог бы работать в ring-0. Но из-за большого количества требуемой памяти возможны всякие глюки. При работе движка, большая часть времени уходит на дизассемблирование, предсказать это время сложно. Исходите из того, что общее время на
обработку одного файла может измеряться минутами, в реалтайме. Ясно, что такой движок нельзя вешать на обычные системные события, типа openfile, также как и не следует вызывать движок из простейшего вируса перед передачей управления хосту. Передача файлов движку должна осуществляться только в отдельной нити или процессе.

 Специальные возможности

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

Библиотека сигнатур

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

Применительно к вирусам, база сигнатур вовсе не должна быть переносима вместе с вирусом; она будет создаваться заново в процессе обработки локальных дисков.

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

Какие преимущества дает библиотека сигнатур? Вроде бы, надо для каждого байта файла делать CMPSB с тысячами сигнатур. Но ведь есть такой рулез, как бинарный поиск. Так что времени уходит мало. А вот качество дизассемблирования должно улучшиться. То есть те процедуры, которые дизассемблер бы пропустил, возможно будут найдены. При этом, при длине сигнатур, скажем, 8 байт, и достаточно большой базе, дизассемблер становится весьма мощной штукой. Ибо конкретные 8 байт навряд ли встретятся в данных, а вот в коде — запросто, и много.

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

Реверсинг в вирусах

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

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

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

Дизассемблирование без реверсинга

Дизассемблировав некоторый исполняемый файл, можно составить «карту» кода в этом файле, то есть выяснить, по каким смещениям в файле находится исполняемый код, а по каким — данные. После этого будет возможно изменить некоторые ассемблерные инструкции в файле на эквивалентные им, но не длиннее исходных. Например заменить XOR AX,AX на SUB AX,AX и наоборот; в результате чего файл останется работоспособным, но изменится его контрольная сумма. В случае, если такой файл был трояном, детектируемым существующими антивирусами, то он, скорее всего, перестанет ими детектироваться. Программы, реализующие такие действия, существуют; но особенно интересно делать это при помощи червя, на удаленных машинах; тем самым плодя недетектируемые модификации bo, netbus’ов и т.п. в огромных количествах.

На этом заканчиваю; надеюсь, вирусные технологии вызывают добрую улыбку на вашем лице… ;-)



©2013 Журнал Хакера Entries (RSS) and Comments (RSS)