Недавно я увидел новость о появлении на GitHub фальшивых репозиториев, которые обманом заставляют жертв скачивать вредонос, угрожающий безопасности их криптоактивов. Вредонос называется Keyzetsu Clipper, и в тот момент мне очень захотелось узнать, как работают настоящие вирусы. До этого у меня только был опыт участия в разных CTF. И тут я понял, что пришло время испытать свои силы на реальном примере.
В данной статье я провел полный анализ и реверс Keyzetsu Clipper, начиная от распаковки и расшифровки до анализа функций персистенца, коммуникации и замены кошельков.
Инструменты
В ходе исследования я использовал следующие инструменты:
IDA Free – дизассемблер и декопмилятор для статического анализа исполняемых файлов,
Python – язык программирования,
de4dot – утилита для деобфускации исполняемых файлов .NET,
dnSpy – декомпилятор для .NET,
VirusTotal – сайт для проверки ресурсов на наличие вредоносных программ.
Поиск экземпляра
Для начала анализа нам нужно найти экземпляр исследуемого вредоноса. Для этого я использовал популярный сайт MalwareBazaar, на который каждый день загружают сотни различных вредоносов.
Скачиваем исследуемый вредонос и меняем его расширение на .bin
. Этот прием позволяет нам случайно на запустить его, а IDA всё также может распознать наш файл как исполняемый.
Первым делом загрузим наш файл на VirusTotal и посмотрим, что он нам скажет.
Как ни странно, 59 из 72 вендоров считают этот файл опасным. Перейдем во вкладку Community
и посмотрим, что нам об этом скажут другие исследователи.
В графе Threat Name
видим название нашего вредоноса – Keyzetsu Clipper, значит мы скачали то, что нам нужно. Также заметим, что вирус стучался к домену API Telegram. Предпологаем, что скорее всего в качестве C2 используется бот в Telegram.
Распаковка
Как и почти любой другой вредонос, Keyzetsu Clipper не распространяется в виде обычного скомпилированного файла. Современные вредоносы обычно пакуются и шифруются для обхода антивирусного ПО. Наш случай не стал исключением, вредонос был упакован до его распространения. Для дальнейшего анализа нам придется распаковать его.
Открываем наш файл в IDA Free. IDA сама перемещает нас на функцию start
. Нажимаем кнопку F5 и смотрим на декомпилированный код.
Сразу видим функцию sub_4013FF
, которой передаются аргументы функции main
. Возвращенное ей значение передается функции exit
. Принимаем эту функцию за main
и продолжаем анализ.
Я осмотрел все три функции и скажу сразу, что нас интересует вторая, которую я назвал func_unpack
. В ней расположен алгоритм распаковки нашего вируса. Переходим сразу туда.
В начале функции мы видим два массива, один с указателями и второй с числами. IDA определила 6 элемент второго массива как указатель, но на самом деле это просто очень большое число и нам просто нужно вручную изменить тип переменной. Некоторые значения по указателям выглядят как си-строки, а значения из массива чисел выглядят как их длины. Скорее всего это зашифрованные строки и их длины, которые в ходе процесса распаковки расшифровываются. Кроме си-строк присутствуют два указателя на очень большие массивы данных. Предположительно это зашифрованные исполняемые файлы.
Дальше видим сам алгоритм распаковки.
Нам примечательна функция sub_401000
, так как в нее передаются наши зашифрованные строки и их длины. Переходим к ней.
После первого взгляда все сразу становится понятно, перед нами обычное шифрование с использованием XOR. Тут же и указатель на ключ шифрования и его длина - 32 байта. Назовем эту функцию func_decrypt_string
.
Я написал скрипт для расшифровки строк на Python.
def decrypt(key, data):
result = []
for i in range(len(data)):
result.append(chr(ord(key[i % len(key)]) ^ data[i]))
# Обычный XOR и преобразование к ASCII символам
return "".join(result)
key = "t:4u]i(g5+9(i=i&,.k)8@cb0udc[1]m"
strings = [
[32, 95, 89, 5],
[32, 91, 71, 30, 21, 6, 91, 19, 120, 74, 87, 73, 14,
88, 27, 117, 73, 92, 29, 64, 91, 37, 77, 7, 72, 16],
[48, 85, 70, 30, 125, 58, 77, 6, 71, 72, 81, 77, 27, 29, 44, 124, 2, 75, 19, 76]
]
# Извлеченные строки в десятичном формате, так как их лечге так копировать из IDA
# Первая строка повторялась несколько раз
for string in strings:
print(decrypt(key, string))
После выполнения скрипта мы получили следующие строки.
Можем предположить, что вредонос распаковывает себя в папку Temp
и создает исполняемые файлы с именами TaskHostManagerService.exe
и Dork Searcher EZ.exe
.
После дальнейшего наименование переменных и расшифровки строк у нас получается цельная картина работы алгоритма распаковки.
Здесь мы сначала мы смотрим сравниваем строки Temp
и CurrentDirectory
(я все перепроверил, именно сравниваем строки), потом мы составляем полный путь до нового файла, который будет расположен в папке Temp
, и сохраняем его в указатель ptr_full_filename
. Затем мы записываем расшифрованное содержимое исполняемого файла. В конце мы запускаем распакованный файл. За 2 итерации мы распаковываем 2 файла, TaskHostManagerService.exe
и Dork Searcher EZ.exe
.
Для дальнейшего анализа нам нужно вручную распаковать исполняемые файлы. Для этого я написал еще один скрипт на Python.
def decrypt(key, data):
result = []
for i in range(len(data)):
result.append(
ord(key[i % len(key)]) ^ data[i])
return result
def unpack_file(file_offset, file_size, unpacked_file_name):
with open("sample.bin", "rb") as file: # sample.bin - исследуемый файл
file.seek(file_offset)
file_data = file.read()
decrypted_data = decrypt(key, file_data)
with open(unpacked_file_name, "wb") as unpacked_file:
for i in range(file_size):
unpacked_file.write(
decrypted_data[i].to_bytes(1, "little"))
unpacked_file.close()
file.close()
key = "t:4u]i(g5+9(i=i&,.k)8@cb0udc[1]m"
first_file_offset = 0xc41
seconde_file_offset = 0x1dce5c
# Смещения запакованных файлов относительно начала исследуемого файла
first_file_size = 1950208
second_file_size = 14200626
# Размеры запакованных файлов из массива
unpack_file(first_file_offset, first_file_size, "TaskHostManagerService.bin")
unpack_file(seconde_file_offset, second_file_size, "Dork Searcher EZ.bin")
По той же логике что и в начале меняем расширения файлов на .bin
. После распаковки получаем два новых исполняемых файла. Dork Searcher EZ.bin
является SFX архивом, который содержит соответствующую утилиту, нас он не интересует. А вот TaskHostManagerService.bin
это и есть наш Keyzetsu Clipper.
Анализ Keyzetsu Clipper
Итак, преступаем к анализу самого вредоноса. Начнем с VirusTotal. Загружаем наш файл и во вкладке Community
видим, что это действительно Keyzetsu Clipper, значит мы на верном пути.
Перейдем к статическому анализу. Запускаем IDA Free и загружаем туда наш файл, но сразу видим ошибку.
Тип cli
скорее всего означает, что наш вирус написан на C#. Для статического анализа файлов .NET
есть несколько решений. Изначально я использовал декомпилятор от JetBrains под названием dotPeek. Его главным минусом является невозможность переименовывать классы, методы, переменные и пространства имен, что делает его практически бесполезным для наших целей. Поискав немного в Интернете я нашел альтернативу, под название dnSpy, декомпилятор .NET
с возможностью переименовывать объекты языка.
Загружаем наш файл в dnSpy и смотрим результат декомпиляции.
Действительно, это исполняемый файл приложения, написанного на .NET
, но похоже, что он обфусцирован. Для деобфускации приложений .NET
существует много различных утилит. Я использовал утилиту de4dot, так как ей проще всего пользоваться.
Загружаем уже новый файл и сравниваем.
Непонятные иероглифы сменились на сокращения. Теперь мы можем начинать наш анализ. Я уже предварительно назвал все методы и классы, так что дальше не буду акцентировать на этом внимание.
Анализ кода
Осмотрев все классы я нашел самые интересные из них. Ключевым классом для понимания работы является класс расшифровки строк и его единственный метод Decrypt
, который даже не был изначально обфусцирован.
Здесь мы видим простой алгоритм расшифровки строк, которые встречаются по всему телу программы. Строка конвертируется из base64 в байтовый массив и "разжимается" с помощью gzip.
Для расшифровки я написал еще один скрипт на Python.
import base64
import gzip
def decrypt(string):
decoded_string = base64.b64decode(string)
return gzip.decompress(decoded_string).decode()
while (1):
encrypted_string = input()
print(decrypt(encrypted_string))
В коде встречается очень много зашифрованных строк, так что подход с циклом while позволяет нам запустить скрипт и расшифровывать нужные строки по необходимости.
Следующий интересный класс – это класс конфигурации. В нем содержатся подставные кошельки и данные для коммуникации с C2. Как мы ранее уже увидели C2 является ботом Telegram. В этом классе расположен его токен. Кроме токена здесь расположен идентификатор чата Telegram.
Увидев токен для бота Telegram я сразу решил посмотреть, как выглядит переписка. На GitHub я нашел проект, написанный для дампа информации с Telegram ботов. Вставляем расшифрованный токен бота и получаем информацию о боте и список сообщений. Самые ранние из них датируются мартом 2023 года. Все сообщения выглядят примерно вот так:
Теперь давайте погрузимся в работу нашего зловреда. Для этого переходим к точке входа, метод Main
.
Тут довольно много различных методов и классов. Теперь нам надо во всем этом разобраться. Пойдем по порядку и начнем с инициализации.
Инициализация
Здесь мы рассматриваем метод Init()
класса Initializer
.
Инициализация проходит только если файл не запущен из папки ProgramData
, то есть при первом запуске. Тут запускается несколько потоков. 3 из них направлены на установку персистенcа, а последний работает с файловой системой.
Вредонос копирует себя в папку ProgramData
и добавляет рандомное количество байт к концу файла. Это сделано, чтобы было сложнее получить его хэш. Далее вирус добавляет ключ в реестр для запуска после загрузки операционной системы. В зависимости от того, запустил ли вредонос администратор или обычный пользователь, ключ добавляется либо в HKEY_LOCAL_MACHINE
либо в HKEY_LOCAL_USER
. После этого вредонос добавляет себя в планировщик задач. За это отвечают первые 3 класса и их методы.
Класс работы с фалами очень большой. Коротко, он ищет файлы, связанные с популярными криптокошельками, архивирует их и отправляет их через бота Telegram. Идем дальше.
В этом фрагменте вредонос отправляет сообщение C2 о том, что была взломана очередная жертва. Потом он запускает копию себя, которая уже находится в папке ProgramData
, а сам завершает свою работу. После этого он удаляет файлы своего присутствия в папке, из которой он был изначально запущен. Делает он это путем создания .bat
файла с командами для удаления и его запуском с задержкой в 7 секунд. Задержка нужна чтобы первый запущенный экземпляр успел завершить свою работу.
Теперь мы переходим к интересному – к замене самих кошельков в локальных файлах и буфере обмена. За это отвечают две различных группы классов. Начнем по порядку, с классов, отвечающих за файлы. За замену кошельков в файлах отвечает класс FilesReplacerClass
и его метод ReplaceWalletsInFiles
.
Замена кошельков в файлах
В данном методе мы передаем списки кошельков, регулярные выражения и пути до файлов методу ReplaceWallets
класса WalletClass
в нескольких потоках. Регулярные выражения разработаны для обнаружения криптокошельков, далее по ним происходит поиск. Пути до файлов берем с рабочего стола и его подкаталогов.
В методе ReplaceWallets
мы ищем совпадение в тексте файла с регулярным выражением и если находим его – меняем на подставной кошелёк. Замена происходит в методе ReplaceWallet
класса WalletReplacer
, который просто меняет настоящий кошелёк на рандомный из списка подставных.
С файлами дела обстоят довольно просто, теперь давайте посмотрим, что там с буфером обмена.
Замена кошельков в буфере обмена
За инициализацию замены в буфере обмена отвечает класс ClipboardInit
который наследует класс Form
. Таким образом создается новая форма. Она нужна, чтобы вызвать метод AddClipboardFormatListener
, который позволит нам реагировать нам события буфера обмена.
Обработкой сообщений формы занимается метод WndProc
, который нужно переопределить для обработки сообщений с использованием пользовательских методов.
В методе WndProc
мы смотрим, есть ли в буфере обмена текст, и запускаем уже знакомые нам потоки для замены кошельков. На этот раз вместо путей до файлов здесь у нас содержимое буфера обмена, а аргументы передаются методуReplaceClipboard
класса ClipboardScanner
.
В этом методе мы также, как в случае с файлами, ищем совпадение с регулярным выражением и передаем его методу Run
класса ClipboardReplacer
, который заменит содержимое буфера обмена.
В отличие от метода замены в файлах здесь все немного интереснее. Если мы просто поменяем криптокошелёк пользователя на рандомный – то он скорее всего заметит это. В файлах это не так важно, потому что человек может просто забыть, что он там оставлял. В буфере обмена такой подход на подойдет, поэтому в классе ClipboardReplacer
есть два метода для сравнения криптокошельков. Если мы сделаем хотя бы первые несколько символов похожими на оригинальный кошелёк, этого будет достаточно, чтобы пользователь не заметил подвоха.
Опуская ненужную логику покажу самые главные фрагменты метода Run
класса ClipboardReplacer
.
В этом фрагменте кода мы формируем HashSet из наиболее подходящих кошельков, которые наиболее похожи на оригинальный кошелёк. Сначала сравниваем символы с левого края. Сравнение происходит в методе CalculateWalletsSimilarityLeft
. Чем больше похожих символов – тем лучше.
После этого повторяем то же самое, но только в этот раз сравниваем символы с правого края. Аналогочно проходу слева, сравнение происходит в методе CalculateWalletsSimilarityRight
.
Таким образом мы находим наиболее похожий на оригинал кошелёк и заменяем его. Пользователь, вставляет кошелёк из буфера обмена и у него нет даже и мысли, что его только что подменили, ведь он смотрит только на первые два или три символа, которые наш вредонос предусмотрительно проверил и нашел подходящую замену.
Заключение
Итак, мы зареверсили и посмотрели, как работает Keyzetsu Clipper. Для меня это был очень полезный опыт, ведь до этого я не имел дела с настоящими вредоносами и приложениями, написанными .NET
.
Я планирую продолжать заниматься реверсом и анализом различных вредоносов, как старых, так и новых. Также я планирую писать подробные разборы по другим вредоносам, с которыми я буду иметь дело.