Создание продвинутых полиморфных движков — Архив WASM.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-эмуляторами. Единственный путь спрятаться - это сделать подобие "случайного" доступа к данной памяти, чтобы надуть эмулятор и заставить их думать, что это часть нормального доступа к приложению, и это то, над чем я долго работал, прежде чем вывел формулу, очень легкую для применения в полиморфизме. Она адаптирована для побайтной расшифровки, но я объясню также, как адаптировать ее для остальных случаев.
Код (Text):
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, но без повторов! Это вроде пермутации последовательности чисел, но вам не нужно хранить одномерный массив или генерировать какие-нибудь данные. Так как формула может рандомизировать все числа, она не очень сильно отличается от "стандартного" декриптора.
Код (Text):
Когда вы будете использовать эту технологию, большинство случайных значений будут от 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, позволит нам победить обычные эмуляторы (конечно, с помощью обычных техник полиморфизма и генерации сложного мусора).
Когда вы взглянете на "законное" приложение, вы можете заметить, что в нем есть много условных переходов, и совершенно нормальным является то, что кусок кода не выполняется столько раз, сколько это делает расшифровывающий цикл. Мы должны решить эту проблему, и это можно сделать так:
Код (Text):
Сначала у нас есть следующие массивы и значения: 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. Первый шаг
Вы должны написать рекурсивную функцию, которую я буду называть "DoBranch". Эта функция должна управлять кодом, как если бы тот был деревом. В движок, туда где начинается конструирование декриптора, вы сначала вставляете инструкции, помещающие в регистры начальные значения. Как только вы ее написали и сгенерировали кое-какой мусор, вы вызываете "DoBranch". Учтите, что поскольку функция будет выполняться несколько раз (в конце концов, она же рекурсивная), не следует использовать фиксированных переменных в памяти. Используйте стек или индексные переменные.
2. Рекурсия рулит!
DoBranch получает контроль и не возвратится, пока с декриптором не будет закончено. Функция должна знать, на каком уровне рекурсии она находится, поэтому вам придется завести переменную, значение которой будет увеличиваться каждый раз, когда будет вызываться "DoBranch" (INC [RecursivityLevel] в самом начале). Каждый раз, когда вы будете возвращаться из функции, вам нужно будет уменьшать значение.
3. Сохраняйте адрес
Ловите момент, когда уровень рекурсии станет максимальным. Если он таким еще не стал, мы сохраняем адрес вставки инструкции в подготовленное нами множество переменных: ArrayOfJumps+ArrayOfJumpsNdx и увеличиваем значение ArrayOfJumpsNdx.
4. Код между переходами
Если вы еще не достигли желаемого уровня рекурсии, после сохранения текущего адреса (пункт 3), генерируйте мусор (немаленькое количество). Когда вы решите, что уже достаточно, сгенерируйте случайный условный переход. Он должен быть очень случайным, как например 'CMP Reg1, Reg2 / JA xxx' или что-нибудь в этом роде, причем Reg1 и Reg2 должны быть мусорными инструкциями (используемыми мусорными инструкциями), если это возможно. Есть огромное количество возможностей (другой возможный вариант - 'TEST Reg, Value / J(N)Z xxx', где Value - это степень от двойки - т.е. установлен только один бит).
Мы должны сохранить адрес созданного нами условного перехода, потому что мы не знаем, куда мы собственно должны перейти, поэтому нам надо будет подпатчить его чуть позже. Достаточно запушить адрес в стек.
Сохранив адрес, мы снова кодируем лист этого бинарного дерева: вы снова вызываете "DoBranch", а когда он возвращается... Вуаля! У нас закодирован полноценный переход, и, конечно, индекс вставки инструкции указывает на место, куда нужно было совершить переход. Поэтому мы достаем сохраненное значение из стека, вычисляем расстояние и сохраняем адрес перехода. После этой маленькой операции мы снова вызываем "DoBranch", уменьшаем [LevelOfRecursivity] и делаем RET.
5. Контроль количества ветвей кода, создаваемых переходами
Количество ветвей, создаваемых этой функции будет около 2^максимальный_уровень_рекурсии, поэтому учтите, что эта функция может сгенерировать огромный декриптор (я рекомендую 3 или 4 уровня рекурсии, что сгенерирует 8 или 16 ветвей).
6. Последний уровень рекурсии
Когда вы достигаете последнего уровня рекурсии, вы генерируете алгоритм расшифровки и все остальное. Убедитесь, что вы делаете это очень случайным образом, потому что поскольку эта часть кодируется 2^(максимальный уровень рекурсии), если ваш код будет очень похож, это не будет полиморфизмом.
Когда вы достигнете пары сравнение/условный_переход (та, которая определяет, продолжать расшифровку зашифрованной части или нет), вам вам необходимо закодировать сравнение и оставить переход так как есть, чтобы подождать, пока вернется функция "DoBranch", дабы быть уверенным, что все ветви уже закодированы. Поэтому мы сохраняем этот адрес в другой подготовленный массив, JumpsToComplete, точно так же, как мы это сделали с ArrayOfJumps.
Затем вставляем кое-какой мусор, кодируем переход на зашифрованную часть и делаем RET.
7. Окончательный возврат из "DoBranch"
После окончательного возврата из DoBranch, нам нужно обработать переходы, которые мы сохранили в JumpsToComplete. В этот раз мы будем использовать два массива, которые мы сконструировали, пока кодировали все ветви. Вся сила этой техники базируется на следующем:
- Получаем первый адрес из JumpsToComplete
- Выбираем случайным образом адрес из ArrayOfJumps
- Обрабатываем адрес из JumpsToComplete таким образом, чтобы у нас получился условный переход на одно из мест дерева (случайным образом).
- Делаем это со всеми адресами, находящимися в JumpsToComplete.
1.3 Внутренняя рекурсия
Мы увидели, что технология ветвления требует рекурсивность для простой реализации этой техники, но так как мы уже сделали ее, мы можем ориентировать весь наш движок на рекурсивные функции, особенно чтобы генерировать косвенные модификации регистра/памяти. Мы собираемся запрограммировать некоторые обычные функции рекурсивным образом, добавив переменную, которую я назвал "уровень рекурсии". Значение этой переменной увеличивается каждый раз, когда вызывается функция. Переменная используется для того, чтобы контролировать активные экземпляры этой функции (поэтому, когда мы достигнем определенного количества вызовом, мы сможем избежать уже ненужного рекурсивного вызова). Давайте посмотрим инструкцию 'MOV Reg,Value' и что случится, если мы напишем эту функцию, которая будет генерировать подобные инструкции рекурсивным образом:
После этого вы сможете увидеть, насколько мощна рекурсивная генерация кода и как простой MOV может превратиться в сложный набор присвоений от памяти к регистру и наоборот, давая в результате нужное значение в нужном регистре. Многие функции можно сделать подобным образом. Позже мы рассмотрим как генерировать мусор.1. Определите тип MOV
Обычно я использую 'MOV Reg, Value', 'PUSH Value/POP Reg' и 'LEA Reg, [Value]', но это я оставляю на вас. У 'MOV Reg, Value' есть другой опкод (C7 C?), но постарайтесь избежать его, так как ни один компилятор не сгенерировал бы его (хотя это прекрасно будет работать, так как это опкод для 'MOV DWORD PTR [Address], Value' в режиме работы с регистрами), к тому же есть более оптимизированные пути сделать это (конкретно однобайтовые опкоды B?).
Теперь, когда у нас есть этот MOV, мы приравниваниваем им "минимальный шанс", используя их, например, в 25% вызовов, и используем достаточно глубокий уровень рекурсии (на мой взгляд, достаточно 5 или 6). Назовем эту функцию "DoMOVRegValue". Поэтому в самом начале мы можем поместить:
Код (Text):
inc byte ptr [RecursivityLevel] cmp byte ptr [RecursivityLevel], 5 jae MakeNoRecursive ...И, наконец, мы делаем переход сюда вместо обычного RET:
Код (Text):
Return: dec byte ptr [RecursivityLevel] ret2. Не только эта функция, но гораздо больше!
Чтобы повысить сложность генерируемого кода, мы должны сделать другие функции по такому же принципу как DoMOVRegValue. Мы можем написать DoMOVRegReg, DoMOVRegMem (которая будет создавать 'MOV Reg,[Address] или нечто вроде этого), DoMOVMemReg и модификации (DoADDRegValue, *SUB*, *XOR* и так далее). Мы должны быть предельно внимательны, чтобы быть уверенными в отсутствии ошибок в коде. Таким образом DoMOVRegValue будет генерировать не только прямые перемещения, о которых мы говорили ранее, но и другие варианты, например:
Код (Text):
; DL=Регистр, который нужно использовать ; EAX=Значение, которое нужно переместить call GiveMeABufferAddress ; EBX=Адрес буфера, в котором мы сохраняем двойное ; слово call DoMOVMemValue ; Используя EBX в качестве адреса ; и EAX в качестве значения call DoMOVRegMem ; Используя EBX в качестве адреса ; и DL в качестве регистра retЭто был самый простой случай, но что, если мы сделаем следующее?:
Код (Text):
call GiveMeABufferAddress call AdjustMemToValue call DoMOVRegMem AdjustMemToValue: mov ecx, eax call Random ; EAX=случайное число sub ecx, eax xchg ecx, eax call DoMOVMemValue ; Перемещаем значение EAX в [EBX] call MakeGarbage mov eax, ecx call DoADDMemValue ; Добавляем EAX к [EBX] retКонечно, мы не должны использовать только ADD, но также и XOR, SUB и так далее. Плюс, мы должны использовать разные типы аргументов:
(Это внутри DoMOVRegValue. Сюда мы попадаем случайным образом, так как есть и другие варианты.)
Код (Text):
call AdjustRegToValue ret AdjustRegToValue: mov ecx, eax xchg ecx, eax call DoMOVRegValue ; Рекурсивный вызов call MakeGarbage mov eax, ecx call DoADDRegValue retТаким образом, возможности безграничны. Мы можем скомбинироваь все созданные нами функции, чтобы генерировать относительно сложные виды MOVинга. Мы можем сделать то же самое с DoMOVRegReg. Мы можем также использовать другие функции: DoPUSHReg, DoPUSHMem и так далее. Давайте взглянем на примеры глубокой рекурсии:
Код (Text):
1 - Мы вызываем DoMOVRegValue и попадаем в: call GiveMeABufferAddress call AdjustMemToValue call DoMOVRegMem jmp Return 2 - So, we execute AdjustMemToValue, which internally calls to DoMOVMemValue and later to DoADDMemValue. Inside DoMOVMemValue we arrive to: 2 - Мы запускаем AjustMemToValue, которая запускает DoMOVMemValue, а потом и DoADDMemValue. Внутри DoMOVMemValue мы попадаем в: call DoPUSHValue call DoPOPMem jmp Return 3 - Executing DoPUSHValue, we arrive here: 3 - Запускаем DoPUSHValue и попадаем сюда: call GiveMeABufferAddress call AdjustMemToValue call DoPUSHMem jmp Return Уф! Мы очень глубоко в рекурсии всех этих функций. Через некоторое время случится выход из AdjustMemToValue, а может быть она вызовет другие рекурсивные функции, которые в свою очередь вызовут AdjustMemToValue. Вот почему мы должны контролировать количество рекурсивных вызовов, так как иначе без особого труда можно сгенерировать огромное количество кода. DoPUSHValue запускает DoPOPMem, которая не является рекурсивной. 4 - После окончательного возврата из AdjustMemToValue, должна быть запущена функция DoMOVRegMem. Эта функция может также запускаться в очень глубокой рекурсии, например: call DoPUSHMem call DoPOPReg jmp Return Мы знаем, что DoPUSHMem не делает рекурсивных вызовов, но следующая функция, DoPOPReg, такие вызовы выполнять может. call GiveMeABufferAddress call DoPOPMem call DoMOVRegMem jmp Return Снова еще больше рекурсивных вызовов <img src="styles/smiles_s/smile3.gif" class="mceSmilie" alt=":smile3:" title="Smile3 :smile3:">.2. Не давайте шанса антивирусным программам
Но даже самый рекурсивный движок в мире может пустить всю работу насмарку, если его можно обнаружить эвристическим методом, потому что он помещает странные инструкции или структуры, свойственные полиморфным движкам. Например:
или что-нибудь в этом роде, так как ни одно нормальное приложение не сделало бы этого.Код (Text):
JMP Next Subroutine: ... ret Next: call Subroutine2.1 Связанные структуры декриптора
Что с ними делать? Все просто: у вас должен быть массив "незавершенных вызовов". Я имею ввиду следующее: вы кодируете инструкцию CALL, но еще не кодируете саму процедуру, на которую ссылается вызов. Затем вы сохраняете адрес этой инструкции в массиве, а в случайном месте или конце декриптора, движок генерирует процедуры и завершает CALL'ы, занесенные в массив, чтобы они ссылались на сгенерированные процедуры. Также хорошим способом может быть прегенерация некоторых процедур до входной точки декриптора и создание ссылающихся на них вызовов (комбинируя этот тип вызовов с "незавершенным" типом).
Таким образом, декриптор будет больше похож на приложение, сгенерированное компилятором, по крайней мере, что касается инструкций CALL. Другой мощный подход состоит в использовании фреймов стека внутри сгенерированных процедур: вы делаете PUSH EBP / MOV EBP, ESP в начале и POP EBP в конце процедуры. На первый взгляд будет сложно определить, является ли декриптор продуктом компилятора или нет. Еще лучше будет, если вы используете стек для передачи значений!
Еще избегайте такого:
Код (Text):
JMP Label x Random bytes Label:Вы думаете, это нормально для обычного приложения? Эмулятор думает точно также . Избегайте подобных вещей, особенно вставки случайных инструкций, на которые никто не ссылается.
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 байтов длиной, мы можем написать функцию для получения случайного адреса памяти, например:
Код (Text):
call Random and eax, 0FCh add eax, [AddressOfMemoryFrame] retВ EAX будет возвращен случайный адрес памяти, выравненный по границе двойного слова.
3.2 API-вызовы
Ок, после того, как мы сгенерировали хорошие структуры кода в декрипторе и доступ к памяти, нам необходимо добавить вызовы API-функций, чтобы обмануть возможные подозрения со стороны AV-программы.
Поместить их не так легко, так как мы должны вызывать только те, описание и формат которых нам известен. Также нам нужно найти их и сосканировать директорию импорта жертвы, поэтому мы должны из виртуального адреса вывести физический (так как мы знаем только о директории импорта из заголовка PE), а затем физический адрес сконвертировать в виртуальный адрес API-функции.
Вот метод, который я использовал (предполагается, что носитель вируса промэппирован в память):
- Мы получаем виртуальный адрес директории импортов, которая находится по адресу PE_заголовок+80h.
- Теперь мы сканируем все секции файла, чтобы найти в какой секции находится нужный нам виртуальный адрес.
- После нахождения секции мы вычитаем виртуальный адрес секции из виртуального адреса импорта, чтобы получить относительную позицию директории импортов в этой секции и добавляем результат к физическому адрес секции, чобы получить физический адрес импорта.
- Теперь мы сохраняем значения, полученные нами, и начинаем сканировать промэппированную директорию импортов, как если бы она виртуальной, так как впоследствии программа будет загружена в память.
- Мы сканируем импортированные модули и ищем знакомые функции, но при этом берем в расчет, что каждый раз, когда мы получаем RVA, мы должны сначала сконвертировать его в физический (это относится и к получению значению из масива относительных виртуальных адресов на имена функций), поэтому, имея RVA, мы вычитаем RVA из секции и добавляем физический адрес этой секции, поэтому мы ищем физический адрес имени функции.
Далее, когда мы находим нужные функции, мы соблюдаем получаем местонахождение текущего элемента массива импортированных функций. Мы добавляем к этому номеру виртуальный адрес этого массива, чтобы получить виртуальный адрес, где будет сохранен импортированный адрес.
- Мы сохраняем этот номер и продолжаем поиск других функций.
После этого мы получаем адреса на импорты, где будут сохраняться виртуальные адреса функций. Теперь вызов вроде CALL [полученный_адрес] будет совершать вызов API-функции. Просто будте внимательны с параметрами и с функциями, которые требуют наличие буфера.
Другая вещь: так программисты из Micro$oft тупы или еще хуже, есть функции, которые могут повесить приложение, например GetModuleHandleA. Я попытался передать ей случайный указатель на имя модуля, чтобы получить его хэндл, но вместо того, чтобы возвратить ошибку вроде "неправильная строка" или "модуль не был найден" или еще что-нибудь вроде этого, возникло исключение, поэтому будьте внимательны с некоторыми функциями.
3.3 Рекурсивные мусорные функции
Мы уже видели потенциал рекурсивных вызовов. Теперь мы применим эт технику к мусору. Есть некоторые виды мусора, которые мы можем делать рекурсивным образом, например CALL'ы, случайные циклы и кое-что еще. Далее я объясню, как генерировать CALL'ы и случайные циклы.
Ранее я объяснял, как делать CALL'ы без создания подозрительных структур. Я говорил, что в конце декриптора (например), вы можете сгенерировать несколько процедур, которые будут вызываться этими CALL'ами. Чтобы создавать процедуры, мы должны использовать рекурсивность, поэтому нам нужно написать рекурсивную функцию DoGarbage. Это необходимо, чтобы внутри вызовов был более лучший мусор. Более того, так мы сможем сделать, чтобы в этих процедурах у нас были другие вызовы. Но будьте внимательны, так как может произойти что-нибудь вроде следующего:
Код (Text):
Subroutine1: ... call Subroutine2 ... ret Subroutine2: ... call Subroutine1 ... retЭто приведет к зависанию декриптора, поэтому приложение никогда не запустится. Чтобы избежать этого, мы должны использовать массивы, чтобы сохранять "уровни" вызовов следующим образом:
Код (Text):
CallsLevel1 db x dup (?) CallsLevel2 db x dup (?) CallsLevel3 db x dup (?) ...Когда мы вступаем в часть движка, отвечающего за генерацию CALL'ов, мы должны увеличить значение переменной, в которой содержится текущий уровень рекурсии, дабы функции одного уровня не вызывали функции того же или более высшего уровня. Тогда мы можем избежать ситуаций, подобных вышеприведенной.
Случайные циклы также генерируются рекурсивным образом. Мы также должны контролировать глубину вложенных циклов, чтобы избежать слишком большоей здержки. Во время генерации цикла мы также должны вызывать DoGarbage, чтобы заполнить цикл (пустой цикл не совсем нормален, как вы знаете).
И, как вы понимаете, мы можем использовать DoMOVRegValue и все такие функции, которые мы написали, чтобы генерировать больше мусора: просто просто отведите регистр под мусор и получите случайное число и используйте эти функции.
4. Напоследок
Ок, эта статья получилась короче, чем я ожидал, но я надеюсь, что она будет полезна вам в плане написания вами новых полиморфных движков. Большая часть идей, описанных здесь, была использована мной в движке TUAREG, поэтому в исходном коде этого движка я иногда ссылаюсь на данную статью. Пока! © Mental Driller / 29#5, пер. Aquila
Создание продвинутых полиморфных движков
Дата публикации 5 янв 2003