JavaScript: деобфускация - как разоблачить скрытый код
Когда JavaScript перестаёт быть понятным
Обычный JavaScript редко выглядит особенно загадочно. В коде можно увидеть функции, переменные, обработчики событий, обращения к API и понятную последовательность действий. Даже если файл минифицирован и собран в одну длинную строку, форматирование обычно быстро возвращает ему привычную структуру.
Совсем иначе выглядит код, который специально подготовили так, чтобы человек не смог понять его с первого взгляда. Вместо нормальных названий появляются переменные вроде _0x3fa2, строки превращаются в наборы чисел и символов, простое действие разбивается на десятки вложенных функций, а настоящий фрагмент программы собирается только во время выполнения.
Браузеру такая конструкция не мешает. JavaScript-движок всё равно получает корректные инструкции и выполняет их в установленном порядке. Проблемы возникают у человека, который пытается выяснить, что именно делает программа.
Такой процесс намеренного усложнения кода называется обфускацией. Его используют для защиты коммерческой логики, усложнения копирования клиентских приложений и сокрытия внутренних алгоритмов. Однако те же методы давно применяются в фишинговых страницах, вредоносной рекламе, заражённых сайтах и браузерных загрузчиках.
Если злоумышленник не может спрятать JavaScript от браузера, он старается хотя бы спрятать его смысл от аналитика.
Обфускация не шифрует программу полностью
Обфускацию часто называют шифрованием кода, но это не совсем точное определение. Полностью зашифрованный файл браузер выполнить не сможет. Перед выполнением данные всё равно придётся преобразовать обратно в JavaScript, а значит, где-то внутри программы обязательно останется механизм восстановления исходных инструкций.
Именно это делает деобфускацию возможной.
Задача обфускатора заключается не в том, чтобы сделать код математически недоступным, а в том, чтобы увеличить стоимость анализа. Если обычную функцию можно понять за минуту, после обработки исследователь может потратить на неё час. Если таких функций несколько сотен, ручной разбор становится заметно сложнее.
Простейший обфусцированный фрагмент может выглядеть так:
var _0x12ab = "aHR0cHM6Ly9leGFtcGxlLmNvbS9hcGk=";
var _0x44ff = atob(_0x12ab);
fetch(_0x44ff);
После декодирования становится понятно, что строка содержит обычный URL:
var apiUrl = "https://example.com/api";
fetch(apiUrl);
Поведение программы не изменилось. Изменился только способ представления информации. Первый вариант заставляет аналитика отдельно изучать строку и функцию atob(), второй сразу показывает назначение кода.
В реальных образцах вместо одного преобразования может использоваться несколько уровней упаковки. Строка декодируется из Base64, затем проходит через XOR, после чего передаётся в eval() и создаёт новый слой JavaScript. Каждый следующий этап становится виден только после обработки предыдущего.
Как код превращают в лабиринт
Самый очевидный способ усложнить чтение - заменить нормальные названия переменных бессмысленными идентификаторами. Вместо serverUrl, userToken и sendRequest появляются _0x14f3, a, b7 и похожие обозначения.
Для компьютера название переменной практически не имеет значения. Для человека оно служит подсказкой. Хорошее имя сразу показывает роль значения, а случайный набор символов заставляет каждый раз вспоминать, что именно хранится внутри.
Следующий распространённый приём - скрытие строк. URL, названия cookies, параметры запросов, команды и сообщения могут храниться в Base64, Hex, массивах чисел или собственных таблицах подстановки. Иногда программа содержит один большой массив, а все строки извлекаются из него по индексам:
const _0x4a21 = [
"fetch",
"https://example.com/api",
"POST",
"stringify"
];
window[_0x4a21[0]](_0x4a21[1], {
method: _0x4a21[2],
body: JSON[_0x4a21[3]]({ status: "active" })
});
После восстановления значений код становится значительно проще:
fetch("https://example.com/api", {
method: "POST",
body: JSON.stringify({ status: "active" })
});
Более сложные обфускаторы изменяют не только внешний вид, но и структуру программы. Последовательная логика превращается в набор состояний, управляемых циклом и оператором switch. Условия инвертируются, простые выражения заменяются ненужными вычислениями, а между полезными блоками добавляется код, который никогда не выполняется.
Особенно характерны конструкции с eval() и Function(). Они позволяют представить программу как строку, преобразовать её во время работы и только затем передать движку JavaScript.
eval("console.log('test')");
В этом примере содержимое видно сразу. Но вместо короткой строки может находиться результат нескольких функций декодирования. Статический анализ видит только вызов eval(), а настоящий код появляется непосредственно перед выполнением.
Ещё одним узнаваемым признаком старых упаковщиков стала конструкция function(p,a,c,k,e,d). Она часто встречается в коде, обработанном Dean Edwards Packer. Внутри такой функции хранятся словарь, закодированная строка и механизм замены токенов. После распаковки получается обычный JavaScript, но до этого исходный файл выглядит как плотная последовательность символов и чисел.
Минификация и обфускация решают разные задачи
Минифицированный код тоже может быть трудным для чтения, но его цель обычно заключается не в защите от анализа. Минификатор удаляет пробелы, переносы строк и длинные названия, чтобы уменьшить размер файла и ускорить загрузку страницы.
Например, исходная функция:
function calculateTotal(price, quantity) {
return price * quantity;
}
После минификации может выглядеть так:
function c(n,t){return n*t}
Читать стало менее удобно, но структура осталась очевидной. Форматирование почти сразу возвращает понятный вид.
Обфускация идёт дальше. Она добавляет преобразования, скрытые таблицы строк, динамическое выполнение и ложные ветки. После обычного форматирования такой код становится аккуратнее, но не обязательно понятнее.
Это различие важно во время анализа. Если перед исследователем обычный минифицированный bundle современного сайта, достаточно beautifier, source map и поиска нужного модуля. Если внутри находятся декодеры, массивы строк и динамическое выполнение, потребуется последовательная деобфускация.
Где искать скрытый JavaScript
Подозрительный код не всегда находится прямо внутри HTML-страницы. Он может быть вынесен во внешний файл через тег script, загружаться после выполнения другого скрипта или создаваться динамически.
Страница способна получить строку с удалённого сервера, создать новый элемент script и добавить его в документ:
const script = document.createElement("script");
script.src = "https://example.com/assets/update.js";
document.body.appendChild(script);
Другой вариант - код хранится внутри HTML как закодированная строка, затем декодируется и передаётся в eval():
const payload = atob("Y29uc29sZS5sb2coJ2hlbGxvJyk=");
eval(payload);
Иногда загрузка зависит от окружения. Скрипт проверяет язык браузера, адрес страницы, наличие определённой cookie, тип устройства или источник перехода. Исследователь открывает страницу вручную и не видит ничего подозрительного, потому что вредоносная ветка активируется только для подходящей жертвы.
Первый этап анализа поэтому заключается не в попытке понять каждую функцию, а в сборе всех доступных частей программы. Нужно изучить HTML, подключённые скрипты, сетевые запросы, динамически созданные элементы и данные, которые появляются во время выполнения.
Код полезно сохранять отдельно вместе с контекстом получения. Один и тот же файл может отдавать разное содержимое в зависимости от заголовков, cookies или времени запроса. Без этих деталей повторить поведение страницы позже будет сложнее.
Форматирование возвращает структуру, но не смысл
Первое действие после извлечения файла обычно выглядит просто: код пропускают через formatter или beautifier. Одна длинная строка разбивается на блоки, появляются отступы, становятся заметны функции, условия и циклы.
Это не деобфускация в полном смысле, но важный подготовительный этап. Пока весь файл находится в одной строке, трудно оценить вложенность и определить границы отдельных конструкций.
После форматирования полезно найти места, где программа создаёт или выполняет новый код. Особое внимание привлекают eval(), Function(), setTimeout() со строковым аргументом, setInterval() со строкой, document.write(), динамическое создание script, а также операции с Base64 и массивами символов.
Поиск таких участков помогает определить точки, в которых один слой обфускации превращается в другой. Вместо чтения всего файла подряд аналитик концентрируется на механизме распаковки.
Если код вызывает функцию, результат которой передаётся в eval(), необязательно сразу выполнять весь скрипт. Иногда достаточно заменить eval() выводом строки:
eval(decodedPayload);
можно временно изменить на:
console.log(decodedPayload);
Теперь код не будет выполнен, а его содержимое можно сохранить и изучить как следующий слой.
Такой подход хорошо работает только в контролируемой среде. Подозрительный скрипт может выполнить опасные действия ещё до вызова eval(), поэтому простая замена одной функции не делает запуск автоматически безопасным.
Переименование переменных восстанавливает логику
После раскрытия строк и удаления первого слоя код часто остаётся неудобным из-за случайных имён. На этом этапе начинается постепенное переименование.
Необязательно сразу угадывать идеальное назначение каждой переменной. Достаточно двигаться от очевидных фактов. Если значение передаётся в fetch(), оно может быть адресом или параметрами запроса. Если объект содержит поля method, headers и body, его можно временно назвать requestOptions. Если функция вызывается после заполнения формы, она может отвечать за обработку или отправку данных.
Например:
function _0x9812(_0x1, _0x2) {
return fetch(_0x1, {
method: "POST",
body: JSON.stringify(_0x2)
});
}
После переименования:
function sendData(endpoint, payload) {
return fetch(endpoint, {
method: "POST",
body: JSON.stringify(payload)
});
}
Само действие не изменилось, но теперь функцию можно понять без повторного чтения каждой строки.
Переименование особенно полезно при работе с большими файлами. Случайные идентификаторы создают постоянную нагрузку на память исследователя. Осмысленные имена превращают анализ в последовательное чтение программы.
Иногда назначение меняется по мере изучения. Переменная, которую сначала назвали userData, позже оказывается объектом с данными браузера и получает имя browserFingerprint. Это нормальная часть работы. Деобфускация не всегда движется по прямой, а понимание программы постепенно уточняется.
Строки часто раскрывают цель скрипта
Даже сложный JavaScript вынужден работать с конкретными значениями. Он обращается к доменам, создаёт имена параметров, читает cookies, формирует пути API и выводит сообщения. Поэтому строки часто становятся самым быстрым источником информации о назначении образца.
Base64 легко узнаётся по характерному алфавиту и знакам = в конце. Шестнадцатеричные строки могут выглядеть как последовательности 68 74 74 70 или escape-последовательности \x68\x74\x74\x70. Unicode-кодирование использует конструкции вроде \u0068\u0074\u0074\u0070.
Строка:
"\x68\x74\x74\x70\x73\x3a\x2f\x2f\x65\x78\x61\x6d\x70\x6c\x65\x2e\x63\x6f\x6d"
после преобразования превращается в:
https://example.com
XOR выглядит сложнее, поскольку для восстановления нужен ключ. Однако ключ должен находиться в самой программе или поступать из доступного источника. Аналитик ищет цикл, который проходит по символам или байтам и применяет оператор ^.
function decode(data, key) {
let result = "";
for (let i = 0; i < data.length; i++) {
result += String.fromCharCode(
data.charCodeAt(i) ^ key.charCodeAt(i % key.length)
);
}
return result;
}
После обнаружения такой функции можно отдельно передать ей зашифрованные строки и получить исходные значения, не запуская остальные части программы.
Кастомные схемы часто оказываются комбинацией простых операций. Символы переставляются, строка переворачивается, числа уменьшаются на фиксированное значение, а затем результат проходит через Base64. На первый взгляд алгоритм выглядит необычно, но после разделения на этапы каждая операция становится понятной.
Лишний код создан, чтобы отнимать время
Обфускаторы нередко добавляют ветки, которые выглядят важными, но никогда не выполняются. Условие может быть всегда истинным или всегда ложным, хотя записано через длинное выражение. Функция вызывается десятки раз, но каждый раз возвращает одну и ту же константу. Массив перемешивается в начале программы только для того, чтобы усложнить сопоставление индексов.
Такой мусор называют dead code или junk code. Его задача - увеличить объём файла и заставить исследователя анализировать несуществующую логику.
Простой пример:
if ((10 * 10) - 50 === 50) {
sendRequest();
} else {
fakeFunction();
}
Условие всегда истинно, поэтому ветка else не используется. После упрощения остаётся:
sendRequest();
В более сложных образцах константные выражения могут состоять из десятков операций и вспомогательных функций. Здесь помогают AST-инструменты, constant folding и частичное выполнение кода.
Однако автоматическое удаление следует применять осторожно. Функция может выглядеть бесполезной, но изменять глобальное состояние, cookies, DOM или замыкание. Перед удалением нужно проверить не только возвращаемое значение, но и побочные эффекты.
Почему подозрительный код нельзя запускать в обычном браузере
Самый быстрый способ узнать поведение JavaScript - выполнить его и посмотреть на результат. Одновременно это один из самых рискованных способов анализа.
Скрипт, открытый в обычном браузере, получает доступ к текущей странице, cookies, локальному хранилищу и сетевым возможностям браузера. Если аналитик запускает его в основной системе, код может отправить запрос на внешний сервер, загрузить дополнительные компоненты или использовать активную авторизацию.
Даже если исходный файл кажется простым, следующая стадия может загружаться динамически. Первый слой только проверяет окружение, после чего получает основной payload.
Анализ проводится в изолированной среде без личных аккаунтов, рабочих cookies и доступа к важным сетям. Сетевые обращения контролируются, а состояние системы можно быстро вернуть к исходному.
При статическом разборе полезно заменять опасные функции безопасными заглушками. Вместо настоящего fetch() можно записывать адрес и параметры запроса. Вместо добавления элемента script - выводить его src. Вместо eval() - сохранять полученную строку.
Например:
globalThis.fetch = async function(url, options) {
console.log("URL:", url);
console.log("Options:", options);
return {
ok: true,
json: async () => ({})
};
};
Такая заглушка помогает увидеть подготовленный запрос, не отправляя его на сервер. Но она не должна создавать ложное чувство безопасности. Код может использовать XMLHttpRequest, WebSocket, изображения, формы и другие способы сетевого взаимодействия.
Сетевые запросы показывают реальное назначение
Локальная логика скрипта иногда выглядит безобидно, пока исследователь не изучит данные, которые программа отправляет наружу.
Вредоносный JavaScript может собирать содержимое формы, cookies, адрес страницы, данные браузера и идентификаторы пользователя. Затем информация упаковывается в JSON, кодируется и передаётся через fetch(), XMLHttpRequest или скрытую форму.
Другой сценарий заключается в загрузке следующего этапа. Небольшой скрипт обращается к серверу, получает новую строку JavaScript и выполняет её через eval() или Function().
fetch("/assets/config")
.then(response => response.text())
.then(code => new Function(code)());
Название /assets/config выглядит безобидно, но ответ сервера фактически становится исполняемым кодом. Для понимания атаки нужно изучить не только исходный файл, но и содержимое ответа.
Во время анализа обращают внимание на адрес назначения, HTTP-метод, заголовки, параметры, тело запроса и условия отправки. Важно понять, какие данные собираются, когда начинается передача и зависит ли поведение от результата сервера.
Иногда домен уже не работает или сервер возвращает пустой ответ. Это не делает образец бесполезным. Структура запроса может показать, какие данные интересовали автора и как была устроена инфраструктура атаки.
Автоматические инструменты не видят весь смысл
Существуют сервисы и утилиты, которые умеют форматировать JavaScript, распаковывать известные packer-конструкции, декодировать строки и упрощать AST. Они заметно ускоряют работу, особенно если образец обработан распространённым обфускатором.
Проблема начинается, когда аналитик воспринимает результат автоматической обработки как окончательный ответ.
Инструмент может раскрыть строки, но не понять, что одна из них является адресом сервера управления. Он способен удалить часть лишних выражений, но не определить назначение формы. Он переименует переменные техническими именами, однако не восстановит бизнес-логику программы.
Современные обфускаторы также используют антиотладочные механизмы. Код проверяет открытие DevTools, измеряет задержки, ищет признаки виртуальной среды или изменяет поведение после вмешательства. Автоматическая распаковка может выполнить не ту ветку и получить поддельный результат.
Поэтому деобфускация остаётся сочетанием инструментов и ручного анализа. Автоматизация убирает механическую работу, а человек устанавливает связи между отдельными частями программы.
Source map иногда делает всю работу за аналитика
Разработчики часто публикуют собранные JavaScript-файлы вместе с source map. Эти файлы нужны для отладки: они связывают минифицированный bundle с исходными модулями, именами функций и структурой проекта.
Обычно source map подключается комментарием в конце файла:
//# sourceMappingURL=app.js.map
Если карта доступна на сервере, браузерные инструменты разработчика могут восстановить структуру исходного проекта. Вместо одного большого bundle исследователь увидит отдельные файлы, оригинальные имена и иногда даже комментарии.
Для обфусцированного кода наличие source map становится серьёзной ошибкой. Разработчик потратил время на сокрытие логики, но оставил рядом карту, которая раскрывает исходную структуру.
Даже без прямого комментария стоит проверять стандартные имена и ответы сервера. Однако делать это можно только в рамках разрешённого тестирования.
Деобфускация строится слоями
Сложный образец редко удаётся понять одним действием. Обычно исследователь постепенно снимает слои.
Сначала код извлекается из страницы или сетевого ответа. Затем форматируется и изучается его общая структура. После этого раскрываются массивы строк, Base64, Hex и собственные декодеры. Динамическое выполнение заменяется сохранением промежуточного результата. Полученный слой снова форматируется, переименовывается и упрощается.
Процесс повторяется до тех пор, пока не останется понятная логика.
Важно сохранять каждый этап отдельно. Если преобразование оказалось неверным или код изменил данные, всегда можно вернуться к предыдущей версии. Кроме того, промежуточные слои помогают документировать анализ и объяснять, каким образом был получен итоговый результат.
Хорошая деобфускация не обязательно восстанавливает оригинальный исходный код символ в символ. Комментарии, исходные имена и структура проекта могли быть потеряны навсегда. Цель заключается в другом - получить эквивалентную программу, поведение которой можно понять и описать.
Что именно нужно выяснить во время анализа
Исследователь не обязан идеально восстановить каждую строку. В реальной задаче важнее ответить на конкретные вопросы.
Нужно понять, какие данные читает программа, какие функции браузера использует, с какими серверами взаимодействует и при каких условиях активируется. Важно установить, создаёт ли она новые элементы страницы, перехватывает ли формы, изменяет ли адреса, загружает ли дополнительный код и пытается ли скрыть выполнение от пользователя.
Если анализ проводится после инцидента, из кода извлекаются индикаторы компрометации: домены, URL, пути, имена параметров, значения cookies и характерные фрагменты строк. Эти данные можно использовать для поиска похожей активности в журналах, сетевом трафике и других образцах.
После этого создаются правила обнаружения и рекомендации по защите. Само восстановление кода становится не конечной целью, а способом понять атаку и уменьшить вероятность её повторения.
Почему навык деобфускации нужен не только malware-аналитикам
Скрытый JavaScript встречается не только в полноценных вредоносных программах. Пентестер может обнаружить его в административной панели, старом клиентском модуле или стороннем виджете. Специалист SOC сталкивается с подозрительным кодом на заражённом сайте. Аналитик фишинга изучает страницу, которая копирует форму авторизации и отправляет введённые данные на внешний сервер.
Даже обычный аудит frontend-приложения часто требует чтения минифицированных bundle. Внутри можно найти скрытые маршруты API, названия параметров, устаревшие функции и логику, которая больше не отображается в интерфейсе.
Деобфускация учит смотреть на JavaScript не как на непроницаемую стену символов, а как на последовательность преобразований. Каким бы странным ни выглядел файл, браузер должен в итоге получить исполняемые инструкции. Задача исследователя заключается в том, чтобы добраться до этого момента раньше выполнения и восстановить смысл программы.
Как устроен модуль «JavaScript: Деобфускация» в Kraken Academy
В Kraken Academy модуль построен вокруг последовательного разбора JavaScript, который специально подготовлен так, чтобы мешать анализу. Пользователь начинает с простых способов сокрытия строк и переименования переменных, а затем переходит к многослойным конструкциям, динамическому выполнению и восстановлению поведения программы.
Главная задача заключается не в запоминании отдельных декодеров. Важно научиться узнавать признаки обфускации, находить механизм распаковки и понимать, какой слой нужно исследовать следующим.
Во время практики пользователь форматирует код, раскрывает Base64 и Hex, разбирает XOR-преобразования, заменяет опасные функции заглушками и изучает подготовленные HTTP-запросы. Постепенно бессмысленные идентификаторы получают понятные названия, ненужные конструкции исчезают, а запутанный файл превращается в последовательную программу.
В модуле предусмотрено 9 практических заданий. Они помогают пройти путь от простых примеров до многослойных JavaScript-головоломок, где каждый следующий фрагмент становится доступен только после правильного анализа предыдущего.
Все задания выполняются в контролируемой среде. Это позволяет экспериментировать с кодом, останавливать выполнение, изменять функции и наблюдать за поведением программы без риска для основной системы.
Скрытый код всё равно должен раскрыть себя
Обфускация создаёт впечатление, что программа превратилась в случайный набор символов. Однако внутри всё равно остаются функции, данные и последовательность действий. Браузер должен восстановить строки, вычислить условия, создать запросы и передать инструкции JavaScript-движку.
Именно в этот момент маскировка начинает работать против своего автора. Если код способен восстановить себя для выполнения, аналитик может изучить тот же механизм и получить читаемый результат.
Чем сложнее упаковка, тем больше потребуется промежуточных шагов. Но общий подход остаётся прежним: не пытаться понять весь файл сразу, а последовательно раскрывать строки, упрощать выражения, изолировать функции и фиксировать каждый новый слой.
Деобфускация начинается не с волшебного инструмента и не с попытки запустить подозрительный скрипт. Она начинается с простого вопроса: какие преобразования должен выполнить сам JavaScript, чтобы скрытый код снова стал исполняемым?
Ответ на этот вопрос обычно и показывает путь к настоящей логике программы.
Эта статья - только начало. Настоящие навыки появляются тогда, когда знания превращаются в практику. В Kraken Academy мы собрали полноценные курсы по информационной безопасности с лабораториями, практическими заданиями и пошаговыми образовательными треками - от первых шагов до уровня специалиста.