·─T─┌O┐─T─┌A┐L···─Z┐┌0┐┌M┐┌B·i┌F─i┌C─┌A┐─T─i┌O┐┬N┬··························
│ │ │ │ ├─┤│ / │ ││││├┴┐┬├─ ┬│ ├─┤ │ ┬│ ││└┤ Issue #1, January-2001
··│·└─┘·│·│·│└─··└──└─┘│·│└─┘││··│└──│·│·│·│└─┘│·│··························
············································································
АВТОМАТИЗАЦИЯ ОБРАТНОГО ПРОЕКТИРОВАНИЯ
(технология недетектируемого вируса)
············································································
Наши усилия направлены на то, чтобы научиться модифицировать
исполняемые программы (в формате PE) так, чтобы поиск внесенных изменений
занимал максимум времени.
Под модификацией понимаем дополнение программы определенным кодом,
скажем, вирусом. Очевидно, что само тело вируса должно быть зашифровано.
Полиморфный же расшифровщик будет интегрирован с кодом программы.
Исходя из этого, задача разбивается на 3 части, или же три глобальных
вопроса: ЧТО?, КУДА? и КАК?.
ЧТО - это вирусные инструкции, которыми будет дополнен инфицируемый PE
файл. О том, какими могут быть эти инструкции, рассказано в статье про
метаморфизм и показано в кодогенераторе.
КУДА - это вопрос о том, в какие места программы вставлять инструкции
расшифровщика, и как определять эти места. В принципе, это относительно
просто; более того, как раз эта часть возлагается на пользователя движка.
В этой же статье будет показано, КАК между двумя произвольными
инструкциями программы вставить инструкции расшифровщика, то есть, другими
словами, как разобрать, изменить и заново собрать всю программу.
ТЕОРИЯ
Нашей задачей является вставить между инструкциями PE файла свои
собственные. Но из-за того, что код и данные в файле могут быть
по-всякому связаны друг с другом, изменение кода повлечет за собой
изменение всех связей. Однако, изменение одних связей влечет за собой
изменение других, и так далее, пока значимая часть файла не будет
изменена.
Пример: вставив в середину файла инструкцию, мы изменим смещения всех
меток, идущих после этой инструкции. А изменив смещение некоторой метки в
коде, необходимо поправить все команды перехода на эту метку. В процессе
такой правки, возможно, увеличатся длины некоторых команд условного
перехода, что повлечет за собой изменение смещений меток, идущих после
этих команд, и так далее.
Связи между элементами файла бывают не только абсолютные (смещения),
но и относительные (команды перехода), и нам необходимо все их выявить.
Выявить абсолютные связи можно посредством анализа структуры PE файла,
включая сюда анализ таблицы фиксапов (релокаций). Для нахождения
относительных смещений требуется разобрать весь код, встречающийся в
программе, на отдельные инструкции.
Исходя из сказанного, необходимо разобрать весь файл в некоторую легко
изменяемую сущность, изменить ее, и собрать файл заново.
Кроме всего прочего, наш алгоритм должен быть оформлен в виде
универсального движка.
То есть, в идеале, я предлагаю вам средство, позволяющее легко
модифицировать структуру PE файла, и чем больше разных методов
модификации этой структуры будет придумано, тем лучше. Кто-то раскидает
декриптор по всему файлу, кто-то интегрирует инструкции декриптора с
инструкциями файла, и т.п.
Итак, нашу задачу можно разбить на 5 этапов:
1. Загружаем PE файл в память по виртуальным адресам; создаем таблицу
флагов и выставляем в ней биты, указывающие, являются ли соответствующие
дворды указателями/длинами, и какими. То есть проводим начальный анализ
структуры файла.
2. Дизассемблируем файл (находим и разбиваем код на инструкции),
попутно заполняя таблицу флагов новой информацией об указателях. Здесь
есть такой ньюанс, что при дизассемблировании можно ошибиться, перепутав
код и данные. Такая ошибка фатальна, поэтому двойственные ситуации
необходимо исключить. Если же выяснить отношение некоторого участка файла
к коду или к данным невозможно, файл обрабатывать не следует.
3. Представляем файл в виде списка из: инструкций, кусков данных,
меток и указателей. То есть от бинарного кода переходим чуть ближе к
исходнику. Такой список создается исключительно из-за легкости
манипулирования его элементами.
4. Вызываем юзерский мутатор (внешний по отношению к движку), который
извращает список, например всовывая куски сгенеренного декриптора между
инструкциями файла.
5. Собираем файл из списка; заново генерим таблицу фиксапов; при
увеличении длин условных переходов пересчитываем все смещения в файле;
пересчитываем контрольную сумму файла.
ПРОБЛЕМЫ ДИЗАССЕМБЛИРОВАНИЯ
На самом деле, конечно, все обстоит много хуже чем кажется: кроме
описанных выше шагов есть еще куча мелочей, каждая из которых влияет на
работоспособность программы. Но основная трудность, естественно,
заключается в дизассемблировании. Ведь мы не обладаем возможностями,
например, иды -- ибо наш вирус ограничен десятками килобайт.
А проблема дизассемблирования вот в чем: у нас есть дворды, про
которые известно, что они фиксапы. Пусть такой дворд показывает в
программу, на какую-то метку. А больше на эту метку не показывает никто.
Вопрос: как узнать, находится ли по этой метке код (какая-то процедура),
либо это данные?
Ошибка влечет за собой следующее: код, принятый за данные, не будет
пофиксен, так что когда он получит управление, сразу произойдет глюк. Ибо
фиксить в этом коде надо jxx, call и т.п. Если же, наоборот, данные будут
приняты за код, и в них подобная инструкция будет пофиксена, то это тоже
чревато глюком.
Поэтому необходимо научиться безошибочно отличать данные от кода.
Некоторые библиотечные процедуры можно было бы найти по сигнатурам, но у
нас нет таких ресурсов. Можно было бы рассматривать jmptable'ы, но это
помогает лишь частично.
Можно было бы работать только с файлами определенного вида, а именно,
такими, в которых в кодовой секции находится только код и ничего больше.
Но это как раз то, чего не хотелось бы делать. Ведь нам надо, чтобы
антивирусы проверяли КАЖДЫЙ файл по пол-часа. Да и мало таких файлов.
Короче говоря, все ошибки в рекомпиляции файла возникают из-за
неправильного дизассемблирования, а именно -- из-за описанной выше
проблемы. Выхода два: ограничить множество обрабатываемых файлов, либо
насколько это возможно улучшить дизассемблер и надеяться на удачу.
ПРАКТИКА
Далее привожу краткое описание всех шагов, необходимых для
рекомпиляции файла.
- проверяем 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-битных приложениях, типа антивирусов и
форматеров. Со всеми проистекающими отсюда глюками.
············································································