allasm.ru |
|
В этой статье предполагается, что вы знакомы с основами создания полиморфных движков, и у вас должны быть неплохие знания о генераторах декрипторов и их создании (это не для новичков! ;) ) В этой статье рассказывается о win32-движках. Я не очень хорошо знаком с вирусами под Linux/Unix, но думаю, что идеи, излагаемые в этой статье могут быть проэкстраполированы на эти операционные системы. 0. Несколько комментариев Эта статья была написана для тех, кто сделал свой полиморфный движок и хочет улучшить свои знания и свою технику, делая еще более лучшие полиморфные движки. Я должен предупредить, что техники, о которых пойдет разговор, требуют очень много времени (ошибка при кодинег, независимо от того, маленькая она или большая, может привести к возникновению огромных ошибок, которые будет очень трудно отследить или которые вылезут в последний момент). Ок, начнем. Я попытался хорошо организовать материал статьи и приводить ясные объснения, но иногда вам может быть трудно меня понять. Просто исходите из того, что я не эксперт в написании статей, я делаю только то, что могу :). 1. Создание более сложных полиморфных движков 1.1 Размер декрипторов Многие люди до сих пор верят в старое правило виркодинга: декрипторы должны быть маленькими. Это правило было верно во времена 40 меговых жестких дисков, но сейчас оно уже устарело. Сейчас у среднего пользователя многогиговый винт, и он/она не знают, сколько на самом деле сейчас свободного места на диске. Поэтому мы можем делать огромные вирусы (10 килобайт или больше) без опасений, что нас обнаружат. Мы можем применить это и декрипторам. Почему бы не сгенерировать 4-х киловый декриптор? Мы можем это сделать и более того - мы чуть ли не обязаны сделать это (100 байтный декриптор для нынешних антивирусных эмуляторов не представляет никакой проблемы). Но мы должны сделать хороший генератор мусора, чтобы антивирусным программ было труднее обнаружить вирус с помощью различных алгоритмических подходов или эвристических техник. Другой плюс больших декрипторов, это то, что эмулятор не сможет с ходу определить, декриптор это или нет. Килобайта мусора в начале декриптора будет достаточно, но учтите, что чем дешевле процессоры, тем меньше времени эмуляторам потребуется для анализа. Мусор выполняется очень быстро во время нормального выполнения, но он может значительно "напрячь" эмулятор. Чем больше мусора вы поместите, тем больше времени потребуется эмулятору и тем меньше вероятность, что эмулятор найдет расшифрованный вирус. Ваш мусор должен быть "корректен", чтобы не сработала эвристика. Также вы должны соблюдать баланс между количеством и качеством кодогенерации (помещение 20 килобайт сложного мусора может замедлить начальную инициализацию приложения, что может привлечь внимание пользователя). 1.2 Алгоритмические приложения Когда это возможно, избегайте линейной расшифровки. Даже если у нас 10-ти килобайтный декриптор, если мы сделаем основной цикл (который легко определяется эмулятором) и будем последовательно обращаться к зашифрованным данным, глупо делать слишком сложный движок, так как многие эмуляторы используют специальную технику, чтобы побеждать сложные декрипторы (они просто помещают брикпоинт после большого цикла и ждут, пока эта часть не будет расшифрованна). Я разработал две техники, призванные предотвратить подобный исход: PRIDE и ветвление. 1.2.1 Технология PRIDE Аббревиатура расшифровывается как Pseudo-Random Index DEcryption (псевдослучайная индексная расшифровка). Идею, лежащую в основе данной технологии, я вынашивал с самого начала, как начал писать полиморфные движки. Из-за нехватки информации мне пришлось исследовать и разрабатывать все самому и теперь делюсь этим с вами. Идея состоит в том, что "нормальная" программа не делает последовательного чтения/записи в какой-либо области данных, как это делают декрипторы всех полиморфных вирусов. Есть несколько техник, которые пытаются избежать этого тем или иным образом (смотрите движок Zhengxi (29A#1) или MeDriPolEn в Squatter(29A#3)), например с помощью добавления нескольких байт, чтобы оставить "дыры", а затем проведения нескольких расшифровок одного и того же кода, но добавляя каждый раз другое число, чтобы в результате полностью расшифровать код. Это также детектится AV-эмуляторами. Единственный путь спрятаться - это сделать подобие "случайного" доступа к данной памяти, чтобы надуть эмулятор и заставить их думать, что это часть нормального доступа к приложению, и это то, над чем я долго работал, прежде чем вывел формулу, очень легкую для применения в полиморфизме. Она адаптирована для побайтной расшифровки, но я объясню также, как адаптировать ее для остальных случаев. Random(число) символизирует случайное число между 0 и число-1 (как в соответствующей C-функции) Encripted_Data_Size = размер зашифрованной части, округленной в сторону ближайшей степени двух (в сторону увеличения) - это я объясню позже. InitialValue = Random(Encrypted_Data_Size) Формула ------- Register1 = Random(Encrypted_Data_Size) Register2 = InitialValue Loop_Label: Decrypt [(Register1 XOR Register2)+Begin_Address_Of_Encrypted_Data] Register1 += Random (Encrypted_Data_Size) AND -2 L-----> Take care with this one! Register1 = Register1 MOD Encrypted_Data_Size Register2++ Register2 = Register2 MOD Encrypted_Data_Size if Register2!=InitialValue GOTO Loop_Label GOTO Begin_Address_Of_Encrypted_Data Вот и все! Очень коротко, очень легко закодировать и очень рандомизировано. Давайте рассмотрим это по шагам, а я объясню математические аспекты формулы (почему именно так и никак иначе): Сначала нужно разобраться, почему зашифрованная часть должна быть степенью 2-х. Если вы посмотрите на формулу, вы можете увидеть, что сгенерированный адрес расшифровки создается с помощью XOR, используя 2 случайных числа. Дело в том, что XOR (в отличии от ADD, SUB и т.д.) никогда не модифицирует бит выше, чем самый высший из двух чисел-операндов. Соответственно, мы можем определить максимальное число-результат (которое всегда будет степенью 2-х). Теперь об используемых регистрах: Register1 используется в качестве модификатора Register2. Каждый раз получается псевдослучайное число, так как мы генерируем начальное значение случайным образом, к которому добавляем в каждом проходе цикла случайное значение. Работа этой формулы осуществляется Register2, и если вы посмотрите на него, вы увидите, что Register2 ничто иное как счетчик, поэтому вы можете увеличивать или уменьшать его значение - это остается на вас (или на ваш движок :) ). Просто держите его внутри заданных ограничений (между 0 и Encrypted_Data_Size). Теперь реальная революция, произведенная данной формулой: после многих тестов я обнаружил, что когда у васе есть счетчик (Register2), и вы ксорите с ним случайное число (всегда в пределах заданных ограничений, я не собираюсь больше этого повторять :) ), вы получите другое число, и если вы добавите к ксорящемуся значение другое небольшое случайное число и увеличите значение счетчика, сделаете XOR со счетчиком в следующий раз, то получите другое случайное значение (гмм, да... - прим. пер.). Когда вы полностью закончите со счетчиком (от нуля до NumberPowerOf2), вы получите последовательность случайных чисел, которые включают все числа от 0 до NumberPowerOf2, но без повторов! Это вроде пермутации последовательности чисел, но вам не нужно хранить одномерный массив или генерировать какие-нибудь данные. Так как формула может рандомизировать все числа, она не очень сильно отличается от "стандартного" декриптора. Когда вы будете использовать эту технологию, большинство случайных значений будут от 0 до размера данных, которые надо расшифровать (степень от 2). В формуле есть одна особенность, необходимая для получения надежных значений: вы должны "выравнивать" числа (то есть результат Random(Encrypted_Data_Size) должны быть кратен 1, если мы расшифровываем побайтно, кратен 2, если расшифроваваем пословно, и 4, если мы расшифроваем по 4 байта (двойное слово)). Но число, которое мы добавляем к ксоращемуся значению, своего рода особенное, потому что оно должно быть кратно 2, если расшифровываем побайтно, кратно 4 для пословной расшифровки, и кратно 8, если расшифровываем подвухсловно. Это можно легко реализовать с помощью получения случайного значения для добавления (до кодирования опкода инструкции), а затем применения над инструкцией следующей формулы: AND Value,Encrypted_Data_Size-2 (for bytes), or AND Value,Encrypted_Data_Size-4 (for words), etc. (в движке, а не в декрипторе!). Просто примите это в расчет, иначе зашифрованная часть будет обработана два раза, что приведет к ее повреждению (я обнаружил это после нескольких часов соцерзания правильного движка и неправильно расшифрованного им кода и после написания тысяч маленьких тестовых программ :) ). Этот метод, хотя он и весьма мощный, можно победить с помощью обнаружения циклов, поэтому мы должны сделать что-нибудь, чтобы избавиться от линейности выполенения декриптора. Самый легкий путь - это поместить несколько условных переходов в середину, но похоже, что эмуляторы обнаруживают, какие области кода более часто выполняются чем другие (или что-то вроде этого), поэтому я подумал об этом и создал следующую технику: 1.2.1 Технология ветвления Эта технология, если скомбинировать ее с PRIDE, позволит нам победить обычные эмуляторы (конечно, с помощью обычных техник полиморфизма и генерации сложного мусора). Когда вы взглянете на "законное" приложение, вы можете заметить, что в нем есть много условных переходов, и совершенно нормальным является то, что кусок кода не выполняется столько раз, сколько это делает расшифровывающий цикл. Мы должны решить эту проблему, и это можно сделать так: Сначала у нас есть следующие массивы и значения: ArrayOfJumps dd N dup (0) ArrayOfJumpsNdx dd 0 JumpsToComplete dd N dup (0) JumpsToCompleteNdx dd 0 ¦ ¦ Это начало декриптора. Это часть, где регистрам задаются начальные ¦ значения, и инициализируется все остальное. ¦ ¦ ¦ x Первый адрес, сохраненный в ArrayOfJumps ¦ ¦ Мусор ¦ .•*•. Случайный условный переход с очень случ. возможностью перехода ¦ ¦ Мусор x x 2ой и 3ий адрес, сохраненный в ArrayOfJumps ¦ ¦ Мусор .*. .*. Случайный условный переход ¦ ¦ ¦ ¦ ¦ ¦ ¦ ¦ Четыре алгоритма расшифровки, которые выполняют одну и ту же ¦ ¦ ¦ ¦ операцию, но используя разный код. ¦ ¦ ¦ ¦ ¦ ¦ ¦ ¦ | | | | Проверка конца расшифровки R R R R Цикл для продолжения расшифровки (переходит случайным образом | | | | на один из адресов, сохраненных в ArrayOfJumps) ¦ ¦ ¦ ¦ Мусор | | | | V V V V Переход на расшифрованный вирус (Это будет сгенерировано с тремя уровнями рекурсии. Просто прочитайте далее объяснение) Я думаю, что диаграмма дает ясное представление о технике, но я дам необходимые пояснения. После того, как мы закончим, у нас будет декриптор, который будет вести себя точно так же, как и обычный, но вы никогда не будет знать наверняка, какая ветвь выполнится, потому что когда программа "прыгает" в цикл, выполняется случайное количество случайныйх сравнений и условных переходов, которые приведут к случайной части расшифровки. Согласно тому, что любая ветвь делает тоже, что и другие, нам неважно, какая из них выполнится. Таким образом мы прерываем линейность выполнения и сейчас декриптор "снаружи" напоминает нормальное приложение, а не цикл декриптора. 1.3 Внутренняя рекурсия Мы увидели, что технология ветвления требует рекурсивность для простой реализации этой техники, но так как мы уже сделали ее, мы можем ориентировать весь наш движок на рекурсивные функции, особенно чтобы генерировать косвенные модификации регистра/памяти. Мы собираемся запрограммировать некоторые обычные функции рекурсивным образом, добавив переменную, которую я назвал "уровень рекурсии". Значение этой переменной увеличивается каждый раз, когда вызывается функция. Переменная используется для того, чтобы контролировать активные экземпляры этой функции (поэтому, когда мы достигнем определенного количества вызовом, мы сможем избежать уже ненужного рекурсивного вызова). Давайте посмотрим инструкцию 'MOV Reg,Value' и что случится, если мы напишем эту функцию, которая будет генерировать подобные инструкции рекурсивным образом: После этого вы сможете увидеть, насколько мощна рекурсивная генерация кода и как простой MOV может превратиться в сложный набор присвоений от памяти к регистру и наоборот, давая в результате нужное значение в нужном регистре. Многие функции можно сделать подобным образом. Позже мы рассмотрим как генерировать мусор. 2. Не давайте шанса антивирусным программам Но даже самый рекурсивный движок в мире может пустить всю работу насмарку, если его можно обнаружить эвристическим методом, потому что он помещает странные инструкции или структуры, свойственные полиморфным движкам. Например:
2.1 Связанные структуры декриптора Что с ними делать? Все просто: у вас должен быть массив "незавершенных вызовов". Я имею ввиду следующее: вы кодируете инструкцию CALL, но еще не кодируете саму процедуру, на которую ссылается вызов. Затем вы сохраняете адрес этой инструкции в массиве, а в случайном месте или конце декриптора, движок генерирует процедуры и завершает CALL'ы, занесенные в массив, чтобы они ссылались на сгенерированные процедуры. Также хорошим способом может быть прегенерация некоторых процедур до входной точки декриптора и создание ссылающихся на них вызовов (комбинируя этот тип вызовов с "незавершенным" типом). Таким образом, декриптор будет больше похож на приложение, сгенерированное компилятором, по крайней мере, что касается инструкций CALL. Другой мощный подход состоит в использовании фреймов стека внутри сгенерированных процедур: вы делаете PUSH EBP / MOV EBP, ESP в начале и POP EBP в конце процедуры. На первый взгляд будет сложно определить, является ли декриптор продуктом компилятора или нет. Еще лучше будет, если вы используете стек для передачи значений! :) Еще избегайте такого:
Вы думаете, это нормально для обычного приложения? Эмулятор думает точно также :). Избегайте подобных вещей, особенно вставки случайных инструкций, на которые никто не ссылается. 2.2 Опкоды, которых нужно избегать Вы когда-нибудь сканировали вирус, который будучи очень навороченным с точки зрения полиморфизма, вставляет однобайтовые инструкции вроде CMC, STI и так далее? Если вы попробуете это, например, с AVP, вы можете заметить, что антивирус автоматически входит в глубокое сканирование. Почему? Потому у него очень сильные подозрения относительно использования подобных инструкций. Кто использует CMC в наши дни? К тому же антивирус осведомлен о том, что генераторы мусорных инструкций могут вставлять множество таких бессмысленных инструкций, поэтому когда он находит относительно большое количество таких инструкций (а некоторые инструкции он отмечает особо, даже если она всего одна), он решает, что файл настолько подозрителен, что его стоит подвергнуть глубокому сканированию. Может быть, это и не страшно, но средний пользователь может подумать, что это нечто больше, чем обычное приложение. Этот совет касается и некоторых 16-ти битных инструкций, используемых win32-приложениями. Когда я писал движок TUAREG, я поместил практически все инструкции, которые мог использовать генератор мусора. Среди них были 8-ми, 16-ти и 32-х битные. Затем, когда я сканировал его с помощью AVP, эмулятор всегда переключался в режим глубокого сканирования. Поразмыслив над этим, я убрал генерацию некоторых 16-ти битных инструкций и AVP не стал напрягаться в этот раз. Я не знаю, какие точно инструкции заставили AVP выставить эвристический флаг, но тем не менее, я рекомендую использовать как можно меньше 16-ти битных инструкций. 3. Продвинутая генерация мусора Сейчас мы перейдем к одной из моих любимых тем: генерации мусора. Лично мое мнение заключается в том, что основной силой полиморфного движка является способность генерировать мусор, так мусор - это код, заставляющий эмуляторы отказаться от отладки или поможет определить им истинную сущность программы. Поэтому, чем более "нормальным" выглядит мусор, тем менее подозрительным выглядит декриптор, а чем сложнее мусор, тем меньше вероятность, что эмулятор сможет отэмулировать декриптор. Давайте рассмотрим некоторые типы мусора. Здесь приведены далеко не все (и, разумеется, не описываются самые простые). Включите ваше воображение! 3.1 Доступ к памяти В наши дни это должно быть в каждом движке, если подрузамевается, что он достаточно сложен. Какие приложения не делают тех или иных обращений к памяти в первых 300 байтах? Только очень странные или зараженные программы с присоединенным полиморфным вирусом, который не использует инструкций записи в памяти (кроме того, что непосредственно касается расшифровки тела вируса). Однако трудность состоит в том, что если в MS-DOS мы имели доступ ко всей памяти и могли читать откуда захотим, в win32 это не так, и попытка прочитать из "откуда захотим", скорее всего, приведет к исключению. Запись в память под win32 еще более затруднена, так как мы может писать только в те секции, которые помечены как WRITEABLE в заголовке PE. Поэтому нам надо использовать некоторые приемы, чтобы иметь области памяти, где бы мы могли и писать и читать. В почти всех исполняемых win32-файлах есть секция, которая называется ".bss". Ее физический размер (место, занимаемое в файле) равен нулю, но виртуальный может быть сколь угодно большим (обычно ее размер равен по крайней мере 1000h байтам, но в большим программах ее размер может доходить до 64K и более). Мы можем использовать эту секцию, чтобы считывать и писать практически все, что угодно, но наш вирус должен всегда запускаться первым, не используя EPO или другие подобные техники, так как приложение должно настроить в этой секции все необходимые для ее нормальной работы данные. Есть другое решение, например, использовать пустые дыры в вирусе, которые мы используем для получения различных сведений в дальнейшем, например, буфер для текущей директории, получаемой с помощью GetCurrentDirectory. Так как надобность в этом поле на определенном этапе отпадает, мы можем использовать его, если оно достаточно велико, так же, как и секцию ".bss", для чтения и записи различных значений. Поэтому как только у нас есть необходимое место, и мы уверены, что оно как минимум 256 или 512 байтов длиной, мы можем написать функцию для получения случайного адреса памяти, например:
В EAX будет возвращен случайный адрес памяти, выравненный по границе двойного слова. 3.2 API-вызовы Ок, после того, как мы сгенерировали хорошие структуры кода в декрипторе и доступ к памяти, нам необходимо добавить вызовы API-функций, чтобы обмануть возможные подозрения со стороны AV-программы. Поместить их не так легко, так как мы должны вызывать только те, описание и формат которых нам известен. Также нам нужно найти их и сосканировать директорию импорта жертвы, поэтому мы должны из виртуального адреса вывести физический (так как мы знаем только о директории импорта из заголовка PE), а затем физический адрес сконвертировать в виртуальный адрес API-функции. Вот метод, который я использовал (предполагается, что носитель вируса промэппирован в память):
После этого мы получаем адреса на импорты, где будут сохраняться виртуальные адреса функций. Теперь вызов вроде CALL [полученный_адрес] будет совершать вызов API-функции. Просто будте внимательны с параметрами и с функциями, которые требуют наличие буфера. Другая вещь: так программисты из Micro$oft тупы или еще хуже, есть функции, которые могут повесить приложение, например GetModuleHandleA. Я попытался передать ей случайный указатель на имя модуля, чтобы получить его хэндл, но вместо того, чтобы возвратить ошибку вроде "неправильная строка" или "модуль не был найден" или еще что-нибудь вроде этого, возникло исключение, поэтому будьте внимательны с некоторыми функциями. 3.3 Рекурсивные мусорные функции Мы уже видели потенциал рекурсивных вызовов. Теперь мы применим эт технику к мусору. Есть некоторые виды мусора, которые мы можем делать рекурсивным образом, например CALL'ы, случайные циклы и кое-что еще. Далее я объясню, как генерировать CALL'ы и случайные циклы. Ранее я объяснял, как делать CALL'ы без создания подозрительных структур. Я говорил, что в конце декриптора (например), вы можете сгенерировать несколько процедур, которые будут вызываться этими CALL'ами. Чтобы создавать процедуры, мы должны использовать рекурсивность, поэтому нам нужно написать рекурсивную функцию DoGarbage. Это необходимо, чтобы внутри вызовов был более лучший мусор. Более того, так мы сможем сделать, чтобы в этих процедурах у нас были другие вызовы. Но будьте внимательны, так как может произойти что-нибудь вроде следующего:
Это приведет к зависанию декриптора, поэтому приложение никогда не запустится. Чтобы избежать этого, мы должны использовать массивы, чтобы сохранять "уровни" вызовов следующим образом:
Когда мы вступаем в часть движка, отвечающего за генерацию CALL'ов, мы должны увеличить значение переменной, в которой содержится текущий уровень рекурсии, дабы функции одного уровня не вызывали функции того же или более высшего уровня. Тогда мы можем избежать ситуаций, подобных вышеприведенной. Случайные циклы также генерируются рекурсивным образом. Мы также должны контролировать глубину вложенных циклов, чтобы избежать слишком большоей здержки. Во время генерации цикла мы также должны вызывать DoGarbage, чтобы заполнить цикл (пустой цикл не совсем нормален, как вы знаете). И, как вы понимаете, мы можем использовать DoMOVRegValue и все такие функции, которые мы написали, чтобы генерировать больше мусора: просто просто отведите регистр под мусор и получите случайное число и используйте эти функции. 4. Напоследок Ок, эта статья получилась короче, чем я ожидал, но я надеюсь, что она будет полезна вам в плане написания вами новых полиморфных движков. Большая часть идей, описанных здесь, была использована мной в движке TUAREG, поэтому в исходном коде этого движка я иногда ссылаюсь на данную статью. Пока! [C] Mental Driller / 29#5, пер. Aquila |