Давай отложим в сторону сухую техническую прагматику и поговорим о прекрасном. Ты спросишь: что может быть прекрасного в обфускации кода? Я отвечу: некоторые решения в этой области обладают прямо‑таки эстетически чарующей красотой, доступной только истинным ценителям. Правда, называются порой не особенно благозвучно. Сегодня мы поговорим о JSFuck — инструменте для обфускации и деобфускации JavaScript.
На днях мне попалась на глаза статья про обфускацию и деобфускацию апплетов JavaScript. Тема эта хоть и малоперспективная, но достаточно актуальная: я и сам когда‑то касался ее в своем материале «Патчим JSXBIN. Как править бинарные скрипты Adobe без перекомпиляции» применительно к адобовской реализации JavaScript в ExtendScript Toolkit. Так вот, статья напомнила мне про старенькое, но незаслуженно забытое решение в этой области народного хозяйства под неприличным названием JSFuck.
Справедливости ради надо заметить, что JSFuck тогда вспомнили исключительно за хайповое название, поскольку хакеры для атаки на eBay пользовались самыми разнообразными обфускаторами и JSFuck там был отнюдь не самым распространенным. Как‑то так получилось, что никто особо не описывал принцип работы этого чудесного алгоритма, отсылая к авторскому ресурсу jsfuck.com. В сегодняшней статье попробую хоть и поздно, но исправить данное упущение.
Началось все с того, что в далеком 2009 году Ёсукэ Хасэгава (Yosuke Hasegawa, не знаю, как этих японских любителей странного и специфичного правильно называть
) создал веб‑приложение jjencode, перекодирующее любой код JavaScript в равноценную исполняемую форму, в которой используется только 18 спецсимволов: []()!+,”$.:;_{}~=.
На первый взгляд подобное кажется чем‑то невероятным — процедурный объектно ориентированный язык высокого уровня ужать до подмножества из 18 спецсимволов, да еще и с сохранением всех исходных имен переменных, классов, методов и так далее…
Представь себе, например, программу на C, состоящую только из спецсимволов, но при этом прекрасно компилирующуюся и корректно работающую. Честно говоря, лично я, когда первый раз услышал про такое, тоже сперва не поверил, думал, речь идет о какой‑то надстройке над интерпретатором, шифрующей и перекодирующей исходный текст или даже подкомпилирующей его в байт‑код. Как, например, в моей статье про PHP «В обход стражи. Отлаживаем код на PHP, упакованный SourceGuardian». Или даже о кастомной реализации JS по типу адобовской.
Однако нет, полученный текст, в котором отсутствуют буквы, цифры и куча других символов, прекрасно и самодостаточно запускается, например, в браузерной строке. В этом легко убедиться самостоятельно, и это действительно выглядит каким‑то колдунством. Подробнее почитать об описанном явлении можно в статье, опубликованной на портале BusinessInfo, но мы не будем углубляться в данную тему, поскольку, оказывается, и это не предел.
В те времена эта идея настолько всех впечатлила, что буквально через полгода на форуме обфускаторов sla.ckers.org был проведен конкурс, цель которого — еще значительнее минимизировать подмножество используемых символов. И действительно, это самое подмножество удалось сократить более чем в два раза, до восьми символов: []()!+,/, а в марте 2010 года из него были исключены и символы ,/.
Полученное минимальное подмножество из шести оставшихся символов использовалось в онлайн‑кодировщике JS-NoAlnum, на базе которого Хасегава в конце этого же года создал кодировщик JSFuck. А чуть позже Мартин Клеппе (Martin Kleppe) опубликовал на гитхабе проект и сделал сайт jsfuck.com с описанием алгоритма.
Самые нетерпеливые читатели могут сразу перейти туда, чтобы ознакомиться с алгоритмом, так сказать, непосредственно из первых уст, для остальных я продолжу свое повествование. Надо отдать должное Хасегаве: он так хулигански назвал свой проект не из каких‑то извращенных побуждений. Название происходит от Brainfuck (переводится как «вынос мозга»), эзотерического языка программирования, на котором программы кодируются схожим набором спецсимволов. Аллюзию добавляет и то, что предшественник языка Brainfuck — эзотерический язык P” тоже использует в своей семантике шесть спецсимволов.
Однако эти необычные языки программирования требовали для своего исполнения компиляторы или интерпретаторы, написанные на других языках, а JSFuck превосходно интерпретирует себя сам при помощи встроенного интерпретатора браузерного движка JavaScript. Начнем же сеанс разоблачения черной магии.
Первый вопрос, который приходит в голову при знакомстве с JSfuck, — как он обходится без цифр и символов? Ведь при помощи его может быть описана любая JavaScript-программа, в которой есть цифры и весь набор латинских (и не только) символов вполне себе присутствует? Оказывается, все просто: генерировать любой символ из шести исходных позволяет характерная особенность языка JavaScript — несколько экстравагантное преобразование типов при бинарных и унарных операциях. Она служит также источником множества мемов и анекдотов про этот язык и программистов на нем.
Начнем с простого. Базовая морфема, описывающая минимальное выражение, сложенное из набора символов []()!+, — это [], означающее пустой массив. При преобразовании его в Boolean (унарной операцией !) мы получаем false, что по‑своему логично, ведь сам пустой массив истинен. Соответственно, двойное отрицание !![] тоже будет истинно, причем булева типа true.
Преобразование пустого массива в число чуть менее логично: +[] дает там 0, и уж совсем нелогично то, что сумма двух пустых массивов []+[] дает пустую строку “”. Оставим шутки над этим для составителей мемов, у нас наметился прогресс — путем суммирования единиц мы получаем все числа, путем суммирования с пустой строкой мы получаем преобразования числа (и любого типа вообще) в строку и строки в число (путем унарной операции +). Таким образом, нам, к примеру, не надо суммировать тысячу единиц, чтобы получить 1000, — достаточно просуммировать “”+1+0+0+0 и преобразовать результат обратно в число.
Но как же теперь быть с символами? Символы можно получать, обращаясь по индексу к строке. Пока что у нас таких строк всего две, они получены при преобразовании в строку булевых выражений true и false. Чуть поднапрягшись, можно получить строки NaN=+[0], undefined=[[]] (даже не спрашивай меня, почему так
) и Infinity=+1e1000, но и это не шибко нас спасает.
В итоге мы имеем довольно куцый набор символов: t, r, u, e, f, a, l, s, I, f, i, n, y, N, d, u. Придется немного помухлевать. Исходя из синтаксиса языка, к любому методу объекта можно обращаться как к индексу массива, используя квадратные скобки. Поскольку [] — это Array, соответственно, [][“flat”] вернет нам метод flat в виде объекта типа Function, который при переводе в строку в браузерной реализации JavaScript выглядит вот так.
На первый взгляд, большинство символов в ней у нас уже имеется, однако добавление фигурных скобок, а также символов c и o заметно сдвинет нас с мертвой точки. Из имеющихся в нашем распоряжении символов мы можем составить замечательное слово constructor, которое дает нам доступ к конструкторам объектов различных типов:
Код:
[][“constructor”] – Array “”[“constructor”] – String 0[“constructor”] – Number true[“constructor”] – Boolean [][“flat”][“constructor”] – Function [][“flat”][“constructor”] – Object
Помимо множества ценных символов, нам открывается еще одна интересная фича: использование конструктора Function позволяет напрямую вызывать собранную строку, наподобие того, как это делает eval. Например, выражение [][“flat”][“constructor”](“alert(1)”)() == Function(“alert(1)”)() напрямую вызывает окно alert, исполняя код в кавычках.Другая интересная находка позволяет нам получить сразу все недостающие латинские символы нижнего регистра от a до z. Известно, что у конструктора Number существует метод toString(base), который преобразует число в строку, используя систему исчисления с базой base, причем base как раз и ограничена количеством строчных латинских букв. Например:
Код:
“k”=(20)[“toString”](21)”
“v”=(31)[“toString”](32)
Чтобы получить вообще все‑все‑все символы и окончательно закрыть тему, нам неплохо бы иметь доступ к методу String.fromCharCode. И здесь мы сталкиваемся с внезапной проблемой, возникшей буквально на ровном месте, — у нас до сих пор нет большой буквы С и совершенно неясно, как ее получить. На удивление, ни одна возвращаемая строка не содержит эту букву. Но хитрый извращенец Хасегава обошел и это! В JavaScript имеется малоизвестный и практически устаревший метод escape, преобразующий спецсимволы в строке в соответствующие шестнадцатеричные escape-последовательности. Например, escape(“<“)=”%3C”, третьим символом полученной строки мы и имеем недостающий нам С, который открывает оставшиеся символы. Бинго!
Теперь, когда вся магия разоблачена, я вкратце расскажу о достоинствах, недостатках и возможностях использования этого диковинного инструмента на практике.
Как легко догадаться, главная проблема JSFuck в его чудовищной объемности. Я сам не проверял, но Вики утверждает, что для кодирования некоторых символов требуется больше тысячи знаков, то есть этот код даже сильно объемнее, чем если просто побитово закодировать каждый символ. При том, как это ни парадоксально, упомянутый факт не влечет за собой тысячекратного замедления работы кода, ведь его сборка — это предварительная разовая операция, после чего код исполняется с обычной скоростью.
Из этого, кстати, вытекает и еще один, гораздо более существенный минус этого способа обфускации. Ведь, несмотря на весь объем и отпугивающую запутанность кода, перед исполнением он собирается в исходный вид, и на данном этапе его легко отследить. Например, рекомендуемые в хабровской статьересурсы для обфускации достаточно легко обмануть, вставив в выражение случайный терм, не входящий в подмножество JSFuck.
Однако встроенный интерпретатор JS не обманешь: как ни крути, на выходе код будет в явном виде.
Конечно, это не означает, что обсуждаемый нами тип обфускации абсолютно бесполезен для практического применения. Если отбросить фанатизм и вместо того, чтобы перекодировать весь код в JSFuck, попутно раздувая его в тысячу раз, технично перемешать рабочий (или обфусцированный по‑другому) код кодированными выражениями, то можно сильно озадачить потенциального реверсера, которому придется вытаскивать и декодировать каждое обфусцированное выражение вручную, в то время как закодировать программу можно и на автомате.
Еще одно «достоинство» (впрочем, временное), которое использовали в свое время хакеры в упомянутой выше атаке на eBay, — отсутствие алфавитно‑цифровых символов, что некоторое время позволяло вредоносному JSFuck-коду проходить фильтры контента.
Гораздо более серьезная проблема этого алгоритма — его привязка к реализации конкретного браузерного движка. Если ты внимательно следил за описанной выше логикой получения отдельных символов, то мог заметить, что, опираясь исключительно на стандарт языка, можно получить не более пары десятков символов, а дальше приходится эксплуатировать недокументированные особенности конкретно взятых браузеров. Ну, например, взятый практически наугад символ q в современных реализациях браузеров уже нельзя получить описанным в мануале способом.
Код:
(“”)[“fontcolor”]([0]+false+”)[20]
Все потому, что (“”)[“fontcolor”]([0]+false+”) — это <font color=”0false”>font> и никакого символа q тут и близко нет, то есть этот символ надо реализовывать описанным мною выше способом через метод toString.Гораздо хуже дело обстоит с небраузерными реализациями JavaScript. Например, в описанной мной ранее адобовской реализации ESTK алгоритм JSFuck попросту невозможен в принципе, в этом легко убедиться самостоятельно.
Как видишь, символы круглых скобок не кодируются классическим способом, а все потому, что в этой реализации почему‑то сократили стандартный метод flat у объекта типа Array. Но даже это не самое плохое — жизненно важные для реализации алгоритма символы c и o в адобовской версии JS стандартным способом получить нельзя, поскольку преобразование функции в строку тут работает иначе.
Тем не менее выкрутиться можно и здесь: если добавить к минимальному набору символов []()!+ два символа фигурных скобок {}, то получим объект типа Object, который при переводе в строку выглядит как [object Object] и дает нам искомые символы c и o. Данный алгоритм называется Hieroglyphy, и подробно изучить его реализацию можно в соответствующей документации. Как видишь, поле деятельности для расширения функциональности и разработки своих собственных реализаций JSFuck путем добавления или замены базового набора символов весьма обширно. В завершение даже подскажу одну идейку: добавление символа $ в адобовской реализации дает объект типа Helper с набором своих методов и свойств.
В браузерной же реализации этот объект хоть и выглядит по‑другому, но тоже весьма интересен.
Таким образом, каждый может самостоятельно реализовать свой собственный уникальный обфускатор, добавив всего один символ к базовому набору.
Leave a Reply