Как деобфусцировать PowerShell-скрипты
Последнее обновление:
Деобфусцировать PowerShell-скрипт — значит слой за слоем отменить преобразования, которые злоумышленник применил, чтобы скрыть, что скрипт на самом деле делает, — и сделать это, ни разу не запустив скрипт. Подозрительная однострочная команда, пришедшая как стена Base64 или как клубок кодов символов и конкатенации строк, под поверхностью обычно оказывается совершенно обычной командой PowerShell: загрузкой, декодированной нагрузкой, командой, которая выполняется в памяти и никогда не касается диска. Обфускация существует не ради хитрости. Она нужна, чтобы обойти сигнатурные сканеры, сделать командную строку нечитаемой с первого взгляда и заставить человека-аналитика сдаться раньше, чем дойдёт до интересного. В этой статье объясняется, зачем нужен каждый слой, как снять его как инертный текст и что на самом деле означают найденные фрагменты. Весь процесс можно пройти в деобфускаторе PowerShell, который декодирует слой за слоем полностью в вашем браузере — ничего не выполняется, и ничто не покидает ваш компьютер.
Зачем злоумышленники обфусцируют PowerShell — и почему закодированную форму нужно снимать первой
PowerShell встроен в каждую машину Windows, может скачивать и выполнять код из интернета, не записывая файл на диск, и работает с правами того, кто его запустил. Это сочетание — причина, по которой это один из самых злоупотребляемых инструментов в фреймворке MITRE ATT&CK, внесённый как техника T1059.001, и причина, по которой первое, что делает злоумышленник с вредоносной командой PowerShell, — спрятать её. Команда в открытом тексте, например DownloadString, за которой следует Invoke-Expression, обнаруживается тривиально: любой сканер, любая проверка журналов, любой аналитик, бросивший взгляд на командную строку процесса, её заметит. Закодированная, сжатая и нарезанная на части, та же команда превращается в строку символов, которая не совпадает ни с одной известной сигнатурой и ничего не значит для человека.
Поэтому закодированную форму нужно снимать первой, раньше всего остального. Пока вы её не декодировали, вы не знаете, смотрите ли вы на установщик, шутку или похититель учётных данных, — а любое другое суждение остаётся догадкой. Хорошая новость в том, что обфускация почти всегда обратима статическим разворачиванием: нагрузка должна быть декодируема машиной, которая её выполняет, поэтому у каждого слоя есть детерминированное обратное преобразование. Base64 декодируется, gzip распаковывается, коды символов снова становятся буквами, склеенные строки соединяются. Работа механическая, а не таинственная, и это самый ценный отдельный шаг при сортировке подозрительного скрипта: одно декодирование превращает «нечитаемый блоб» в читаемую команду, о которой можно по-настоящему рассуждать.
-EncodedCommand и UTF-16LE: почему Base64, похожий на абракадабру, обычно является читаемым текстом
Самая распространённая форма обфускации PowerShell, с которой вы когда-либо столкнётесь, — один переключатель: powershell.exe -EncodedCommand, за которым следует длинная строка Base64. Она выглядит как случайные символы, но почти всегда это просто обычный текст скрипта, закодированный способом, который в первый раз удивляет. -EncodedCommand в PowerShell ожидает, что Base64 декодируется в UTF-16LE — два байта на символ, с нулевым байтом после каждой буквы ASCII, — потому что такова кодировка строк .NET в Windows. Так, Base64 безобидной команды Write-Host "hello" начинается с SQB3AHIAaQB0AGUALQBIAG8AcwB0ACA…, а характерный признак — повторяющиеся символы «A», происходящие от всех этих нулевых байтов.
Зная это, вы по-другому читаете закодированный блоб. Строка Base64, декодирующаяся в UTF-8-абракадабру, скорее всего, вовсе не UTF-8: попробуйте сначала UTF-16LE, и «абракадабра» превратится в читаемый PowerShell. То же относится и к голому блобу Base64 без видимого переключателя -EncodedCommand — длинные токены допустимого Base64, декодирующиеся в печатный текст, — это тот же трюк без флага. Практическое правило простое: увидев длинный токен Base64 в командной строке PowerShell, считайте, что это скрипт, пока декодирование не докажет обратное, и декодируйте его инертно — как текст, в инструменте, который никогда его не выполнит, — вместо того чтобы вставлять его в окно чата или терминал, чтобы посмотреть, что произойдёт.
Слоёный пирог: сжатие, коды символов, конкатенация, -f, обратные кавычки
Реальные нагрузки редко останавливаются на одном слое. После декодирования -EncodedCommand скрипт под ним часто сам оказывается закодирован ещё раз: сжатый gzip-блоб, обёрнутый в ещё одну строку Base64, или последовательность кодов символов вроде [char]68+[char]105, которая произносит команду по цифрам, или строка, нарезанная на фрагменты и склеенная обратно знаками плюс, или форматная строка, в которую оператор -f подставляет части во время выполнения, или обратные кавычки, рассыпанные по ключевым словам, чтобы Do`wnloadString больше не находился наивным поиском DownloadString. Каждый слой существует по своей причине. Сжатие сжимает длинную нагрузку в короткую однострочную команду и полностью побеждает текстовый поиск. Коды символов и конкатенация побеждают сопоставление сигнатур, гарантируя, что подозрительные слова никогда не встречаются в файле буквально. Оператор -f и обратные кавычки побеждают простой поиск строк, сохраняя скрипт синтаксически допустимым.
Защита от всех них одна и та же: разворачивать по одному слою за раз, по порядку, и останавливаться, когда больше ничего не применимо. Декодируйте Base64, затем проверьте, сжат ли результат, и распакуйте его, затем разрешите коды символов, соедините фрагменты, подставьте аргументы формата и уберите обратные кавычки — каждый шаг даёт обычный текст, с которым может работать следующий шаг. Инструмент, который делает это как видимую временную шкалу — по карточке на слой, с «до» и «после», — ценнее инструмента, который молча выдаёт конечный ответ, потому что сама временная шкала является доказательством: она показывает, какие слои присутствовали, а когда разворачивание останавливается раньше времени — «не удалось развернуть дальше», — это находка, а не сбой. Она говорит вам, что нагрузка содержит нечто динамическое, зашифрованное или собираемое во время выполнения, чего один статический анализ достичь не может.
Журнал блоков скриптов 4104: как длинные скрипты разбиваются и собираются обратно
Есть один источник деобфусцированного PowerShell, который вообще не требует декодирования: сама машина. Когда журнал блоков скриптов включён, Windows записывает содержимое каждого блока скриптов, обрабатываемого PowerShell, как событие 4104 в журнал Microsoft-Windows-PowerShell/Operational — и записывает код таким, каким его увидел PowerShell, после того как движок уже развернул -EncodedCommand и слои обфускации. Подозрительная однострочная команда, пришедшая как Base64, появляется в событии 4104 как читаемый скрипт, который она на самом деле выполнила. Поэтому эта функция — самая ценная настройка журнала PowerShell, которую может включить защитник, и поэтому же она отключена по умолчанию: она многословна, а в журналах могут содержаться учётные данные и другие чувствительные данные, поэтому Microsoft рекомендует включать защищённый журнал событий вместе с ней для всего, кроме диагностики.
Подвох в том, что Windows разбивает длинные блоки скриптов на несколько событий 4104. Каждое событие несёт тот же ScriptBlockId, MessageNumber и MessageTotal — например, часть 2 из 5, — а полный скрипт существует только тогда, когда части сгруппированы по этому идентификатору, упорядочены по номеру сообщения и соединены. Одно событие в отдельности может оборваться на полуслове, а поиск, читающий только первый фрагмент, пропустит то, что нагрузка делает в конце. Поэтому повторная сборка — механический шаг, в котором нужно проверить только одно: не пропущена ли какая-то часть. Если журнал говорит «2 из 3», а часть 2 так и не пришла — потому что журнал провернулся или был очищен, — собранный текст неполон, и любой вывод из него предварителен. Та же логика группировки используется в просмотрщике EVTX, когда он помечает события 4104 в экспортированных файлах .evtx; см. также сопутствующую статью Чтение файлов .evtx без машины Windows.
Чтение индикаторов: загрузочные «колыбели», обход AMSI, обход политики выполнения
Когда скрипт стал читаемым, следующий шаг — не объявить его вредоносным, а прочитать содержащиеся в нём индикаторы как наводки, которые нужно проверить. Загрузочная «колыбель» — DownloadString, DownloadFile, Net.WebClient, Invoke-WebRequest, bitsadmin, certutil, забирающие файл по URL, — говорит, что скрипт получает код откуда-то, и следующий вопрос — откуда и ожидаемо ли это назначение в вашей сети. Обход AMSI — amsiInitFailed, AmsiUtils или код, исправляющий интерфейс антивирусного сканирования, — говорит, что автор хотел, чтобы скрипт выполнялся без сканирования, что является сильным сигналом, но не приговором: пентестеры и некоторые легитимные инструменты делают то же самое. Обход политики выполнения (-ExecutionPolicy Bypass, -ep bypass) говорит, что автор не хотел, чтобы собственные защитные механизмы PowerShell останавливали скрипт, что встречается как во вредоносном ПО, так и в обычной автоматизации, запускающей неподписанные скрипты.
Дисциплина — относиться к каждому индикатору как к вопросу, а не как к ответу. Загрузочная «колыбель» плюс скрытое окно (-WindowStyle Hidden) плюс закодированная команда — это паттерн, который стоит эскалировать; любой из них в отдельности — паттерн, который стоит проверить. То, что превращает наводку в приговор, — контекст: кто запустил процесс, каким был его родительский процесс, известен ли URL, с которым он связывается, появляется ли тот же скрипт на других машинах. Деобфускатор, который перечисляет индикаторы с совпадающим фрагментом и пометкой «проверьте в контексте», выполняет честную работу; тот, который штампует скрипт как «вредоносный» или «чистый» по списку шаблонов, преувеличивает то, что может знать статический анализ. И обратное тоже важно: скрипт без найденных индикаторов не доказан как безопасный — он может просто использовать технику, которую список не охватывает, или собирать свою нагрузку динамически во время выполнения.
Безопасный процесс: никогда не вставляйте в чат, никогда не запускайте, декодируйте инертно
Самое важное правило обращения с подозрительным PowerShell-скриптом: никогда не запускайте его, чтобы узнать, что он делает, и никогда не вставляйте его в окно чата, чтобы спросить ИИ. Запустить его — значит выполнить нагрузку, а это именно тот результат, которого вы пытаетесь избежать. Вставка в чат отправляет потенциально вредоносный скрипт стороннему сервису, оставляет его в истории разговора, которую вы не контролируете, и часто всё равно приводит к отказу, потому что системы ИИ регулярно отказываются деобфусцировать скрипты, которые могут быть вредоносными. Безопасный процесс намеренно скучен: скопируйте блоб в браузерный локальный деобфускатор или на изолированную аналитическую машину и относитесь к декодированному выводу как к инертному тексту для чтения, а не как к коду для выполнения.
Дальше процесс — короткий контрольный список. Первое: декодируйте слой за слоем, пока текст не перестанет меняться, и запишите, где остановилось разворачивание. Второе: прочитайте индикаторы и запишите, что означал бы каждый из них, если бы он был верен, затем проверьте каждый по контексту — родительский процесс, целевой URL, машину, с которой он пришёл. Третье: передайте результат людям или системам, созданным для следующего шага: вашей консоли EDR, детонации в песочнице, вашей команде реагирования на инциденты. Работа деобфускатора заканчивается на читаемом тексте и честных индикаторах; приговор принадлежит песочнице и аналитику, а не инструменту декодирования. Если скрипт пришёл с машины, которой вы управляете, проверьте, был ли включён журнал блоков скриптов — если был, события 4104 являются базовой истиной о том, что выполнялось, и их стоит собрать до того, как журнал провернётся.