среда, 6 июня 2018 г.

Пишем JSFuck-транслятор

  1. О чем пойдет речь
  2. Базовые понятия
  3. Приступим к написанию кода
  4. Дополним базовый словарь
  5. Работа с функциями
  6. Использование encodeURI вместо escape
  7. Несколько мыслей по поводу оптимизации вывода
  8. Оптимизация работы с fromCharCode
  9. Немного тестов

О чем пойдет речь



Для начала цитата из Википедии
Цитата:
JSFuck is an esoteric programming style of JavaScript, where code is written using only six characters: [,], (, ), !, and +. The name is derived from Brainfuck, an esoteric programming language which also uses a minimalistic alphabet of only punctuation. Unlike Brainfuck, which requires its own compiler or interpreter, JSFuck is valid JavaScript code, meaning JSFuck programs can be run in any web browser or engine that interprets JavaScript.
Цитата:
JSFuck - эзотерический программный стиль языка JavaScript, код которого написан всего шестью символами: [,], (, ), !, и +.
Имя произведено от Brainfuck, эзотерического языка программирования, который также использует минималистический алфавит, состоящий исключительно из знаков пунктуации. В отличие от Brainfuck, для которого требуется собственный компилятор или интерпретатор, JSFuck является валидным кодом JavaScript, что подразумевает, что JSFuck-программа может быть запущена в каком-либо браузере или движке, который интерпретирует JavaScript.
JSFuck - Wikipedia
Существует ресурс JSFuck - Write any JavaScript with 6 Characters: []()!+, где можно перекодировать обычный JavaScript в JSFuck и запустить его либо там же либо в браузере. Есть также на гитхабе полный листинг готового транслятора.

Отсюда возникает вопрос: зачем я все это пишу, если все это уже есть в готовом виде и скорей всего оно даже лучше того, что в конце концов получится у меня? А пишу я это потому, что сам хочу разобраться в вопросе. Когда я впервые увидел код JSFuck, запустил его в браузере и увидел, что это работает, я не поверил своим глазам. То есть мне было понятно, что можно что-то построить, что будет возвращать текст программы, но чтобы его запустить, надо какие-то ключевые слова писать или хотя бы eval вызвать или что-то подобное. Но нет же: код, состоящий исключительно из символов []()+! работал без каких-либо дополнительных костылей. Поэтому я решил, что чтобы понять, как это все происходит, надо самому написать транслятор JavaScript-to-JSFuck. Я не ставлю цели написать что-то оптимальное по любому параметру, главное - это чтобы с его помощью можно было перекодировать JavaScript в JSFuck и чтобы это работало в браузере.

Базовые понятия



Для начала разберемся, откуда вообще в этом нагромождении одних и тех же символов может взяться что-то вразумительное типа букв или цифр. Секрет в том, что язык JavaScript - динамически типизированый и если выполняется какая-то операция над объектами, которая для объектов этих типов не предусмотрена, то движок языка пытается хоть как-нибудь это дело интерпретировать. В результате иногда выдает что-то, что мы можем использовать для решения нашей задачи.

Во-первых, рассмотрим символы, которые есть в наличии, и посмотрим, что с ними можно сделать.
[] - квадратные скобки могут использоваться как литерал массива и таким образом мы можем, просто написав их, создать экземпляр массива. Кроме того они могут использоваться для обращения к методам и свойствам объектов, а также индексам массивов. Возможность обращаться к методам и свойствам здесь критически важна, поскольку точки у нас нет, да и не помогла бы она, поскольку нет таких имен в JavaScript, которые состояли бы из имеющихся у нас символов.

() - с круглыми скобками все более-менее понятно, поскольку весь код у нас будет одним большим выражением, то группировка различных его частей - критически важна.

! - логичское НЕ, но не только. Поскольку в JavaScript, как уже упоминалось выше, движок все пытается как-то интерпретировать, а данный оператор применяется только к типу Boolean, то любое выражение, к которому будет применен этот оператор, движок будет конвертировать в Boolean, то есть это еще и конвертер типов.

+ - данный оператор помимо сложения чисел и конкатенации строк, также может использоваться как конвертер типов. Унарный плюс конвертирует операнд к типу Number, а бинарный нередко может использоваться как конвертер к типу String.

Теперь посмотрим, что можно сделать. Поскольку единственный объект, который можем создать на данном этапе - это массив, то с него и начнем.

[] - это просто массив. А если сложить два массива []+[], то получится пустая строка. Происходит этот потому, что для массива не определен оператор +, поэтому движок конвертирует оба объекта в строку и складывает строки. Почему строка оказывается пустой? Ну вот взять перевести в строку следующий массив [1, 2, 3].toString() и получим строку 1, 2, 3, то есть элементы массива будут выписаны через запятую без скобок. А что выдаст пустой массив? То же самое, только это и будет пустая строка. А две пустых строки при конкатенации дают пустую строку.

Теперь попробуем применить к массиву операторы.
![] - false
+[] - 0
А если к false применить !, то получится true
!![] - true
Мы уже выяснили, что если мы используем оператор + для объектов, для типов которых он не определен, то он сначала будет превращать все в строки. А стало быть
[]+![] - "false"
[]+!![] - "true"
Кроме того, если запросить несуществующий ключ объекта любого типа, то мы получим undefined. Ну например
[][[]] - undefined
Само по себе это мало что дает, а вот вот это пригодится
[]+[][[]] - "undefined"
Далее числа
+[] - 0
+!+[] - 1
+!+[]+!+[] - 2
+!+[]+!+[]+!+[] - 3

Теперь, имея в арсенале базовый набор слов и чисел, мы можем получать символы и составлять из них слова.
"false"[0] - "f"
"true"[1]+"true"[2]+"false"[2]+"true"[3] - "rule"
Или можно так
([]+!![])[+!+[]]+([]+!![])[+!+[]+!+[]]+([]+![])[+!+[]+!+[]]+([]+!![])[+!+[]+!+[]+!+[]] - "rule"

Таким образом мы составили из нашего скудного алфавита простое слово и можем двигаться дальше.

Приступим к написанию кода



Создадим класс JSFuck и в конструкторе определим базовый словарь, состоящий из слов, которые можно получить построив простые выражения. На данный момент у нас есть три таких слова true, false и undefined.
Код javascript Выделить
    class JSFuck
    {
        constructor()
        {
            this.basics = {
                "true": "([]+!![])",
                "false": "([]+![])",
                "undefined": "([]+[][[]])"
            }
        }
    }
Учитывая, что нам понадобится индексировать слова, приступим к созданию методов, с помощью которых можно будет генерировать коды чисел. Создадим два метода: getDigit - для получения кода цифры, и getInteger - для получения кода целого числа. Выше я уже показывал, что числа можно получать путем сложения единиц, то есть +!+[] - единица, можно повторять это несколько раз и единицы будут склаываться. Но для генерации больших чисел это не очень удобно. Поэтому лучше опсанным способом создавать отдельные цифры, цифры преобразовывать в строки, строки складывать и, при неободимости, опять переводить в числа. То есть для создания числа 123 не нужно складывать единицу 123 раза. вместо этого мы возьмем единицу, отдельно, две единицы отдельно и т. д. То есть +("1"+"2"+"3"). Таким образом цифры мы будем генерировать хоть и в числовом формате, но при этом будем упаковывать их в массивы. Потому что при выполнении операции [1]+[2]+[3] мы получим строку "123". Таким образом функция getDigit будет выглядеть так
Код javascript Выделить
            getDigit(d)
            {
                d = +d;
                return "[" + (d == 0 ? "+[]" : Array.apply(null, { length: d }).fill("+!+[]").join("")) + "]";
            }
Она принимает число, а возвращает строку, представляющую массив с нужным количеством единиц или ноль, например получает 3, а возвращает "[+!+[]+!+[]+!+[]]". Теперь из цифр можно строить числа
Код javascript Выделить
            getInteger(n)
            {
                return n.toString().split("").map((v) => this.getDigit(+v)).join("+");
            }
Этот метод принимает целое неотрицательное число и строит из него код, складывая цифры, как описанов выше.

Имея базовый словарь и средство построения кода чисел, мы можем написать метод encodeText, который будет строить слова из букв, входящий в слова, содержащиеся в словаре.
Начнем кодировать текст.
Код javascript Выделить
        encodeText(text)
        {
            let digits = "0123456789";
            let allChars = Object.keys(this.basics).join("").split("").filter((v, i, a) => a.indexOf(v) == i);
            let result = [];
            for (let char of text)
            {
                if (allChars.includes(char))
                {
                    for (let w in this.basics)
                    {
                        let index = w.split("").findIndex(c => c == char);
                        if (index > -1)
                        {
                            result.push(`(${"([]+" + this.basics[w] + ")"}[${this.getInteger(index)}])`);
                            break;
                        }
                    }
                }
                else if (digits.indexOf(char) > -1)
                {
                    result.push(this.getDigit(char));
                }
                else
                {
                    return "";
                }
            }
            return result.join("+");
        }
Здесь мы обходим текст посимвольно и проверяем сначала его наличие в словаре: если проверка удачна, то находим в словаре слово с этим символом, и формируем код, который добавляется в массив с результатом. Если символ не прошел проверку, то проверям, не является ли он цифрой, поскольку для них у нас тоже есть метод кодирования. Если ни одна проверка не прошла, то пока будем завершать функцию с возвращением пустой строки. Когда мы создадим другие инструменты, мы допишем дополнительную логику.

Дополним базовый словарь



На данный момент уже можно поэкспериментировать с полученным классом и сгенерировать те слова, которые можно составить из имеющихся в словаре символов.
Понятно, что с теми буквами, которые есть у нас сейчас - "далеко не уедешь", так что нам нужно немного развиться, чтобы можно было составить некоторые слова, необходимые для дальнейших действий. Сейчас нам нужно из имеющихся букв получить что-то что даст нам еще больше букв. На данный момент мы можем получить ссылку на один из методов массива. Для этого нам нужно составить имя метода и воспользоваться тем, что JavaScript позволять получать доступ к членам объекта не только с помощью точки и имени, но и с помощью квадратных скобок и строки представляющей имя. То есть вместо [].push мы можем написать []["push"] и это будет иметь тот же эффект. Из имеющихся символов мы можем составить, например, такие слова как fill или filter и этого для данного этапа будет достаточно. Превратив ссылку на функцию в строку уже известным нам способом, мы получим довольно много новых букв. Выражение []+[]["fill"] даст нам следующую строку
"function fill() {
[native code]
}"

Здесь у нас и скобки трех видов и пробел и несколько новых букв, так что добавим эту строку в наш арсенал базовых элементов. И на данный момент наш конструктор класса выглядит вот так
Код javascript Выделить
        constructor()
        {
            this.basics = {
                "true": "([]+!![])",
                "false": "([]+![])",
                "undefined": "([]+[][[]])"
            }
            this.basics["" + [].fill] = `([]+[][${this.encodeText("fill")}])`;
 
        }
И теперь у нас появилась возможность закодировать, например, такое важное слово, как constructor, поскольку все необходимые буквы у нас уже есть. И если сделать вот так "" + [].fill.constructor(), то можно получить следующее
"function anonymous(
) {

}"

Не густо, но там есть пара букв, которые нам тоже пригодятся. Так что дополним код
Код javascript Выделить
    this.basics["" + [].fill.constructor()] = `([]+[][${this.encodeText("fill")}][${this.encodeText("constructor")}]())`;
Также можно добавить еще один словарик, в котором будем хранить слова, исползуемые целиком.
Код javascript Выделить
        this.words = {};
        let constructor = this.words.constructor = this.encodeText("constructor");
        let name = this.words.name = this.encodeText("name");
        let tostring = this.words["toString"] = this.encodeText("to") + `+(([]+[])[${constructor}][${name}])`
Добавили слова constructor, name, toString. Последнее особенно актуально, поскольку это более короткая версия, чем построение по буквам, да и для получения символа S все равно пришлось бы "тянуть" конструктор строки. В базовый словарь слово String тоже можно добавить.
Код javascript Выделить
this.basics["String"] = `+(([]+[])[${constructor}][${name}])`;
На данном этапе, когда у нас появилось слово toString мы получили доступ ко всем буквам латинского алфавита в нижнем регистре. Теперь нам совсем не обязательно "выковыривать" каждую букву изо всех "дыр", в которые они могли затесаться. Для того, чтобы понять, что я имею в виду, надо вспомнить о том, что у типа Number метод toString может принимать аргумент, указывающий на то, в какой системе счисления нужно представить число при переводе его в строку. Поддерживаются системы от 2 до 36. В качестве недостающих цифр в системах с большими основаниями используются буквы латинского алфавита в нижнем регистре. И в системе с основанием 36 используется весь алфавит. Числа мы формировать умеем, toString у нас теперь есть, так что буквы алфавита будут лежать в диапазоне от 10 до 35 и получить их можно вот такими выражениями
Код javascript Выделить
(10)["toString"](36) // a
(11)["toString"](36) // b
....................
(34)["toString"](36) // y
(35)["toString"](36) // z
Соответственно в наш метод encodeText нужно добавить еще одну ветку с условием, и если символа нет в базовом словаре и это не цифра, то будет проверяться не является ли он латинской буквой в нижнем регистре. Таким образом текущая версия этого метода будет следующей.
Код javascript Выделить
        encodeText(text)
        {
            let alphal = "abcdefghijklmnopqrstuvwxyz";
            let digits = "0123456789";
            let allChars = Object.keys(this.basics).join("").split("").filter((v, i, a) => a.indexOf(v) == i);
            let result = [];
            for (let char of text)
            {
                if (allChars.includes(char))
                {
                    for (let w in this.basics)
                    {
                        let index = w.split("").findIndex(c => c == char);
                        if (index > -1)
                        {
                            result.push(`(${"([]+" + this.basics[w] + ")"}[${this.getInteger(index)}])`);
                            break;
                        }
                    }
                }
                else if (digits.indexOf(char) > -1)
                {
                    result.push(this.getDigit(char));
                }
                else if (alphal.indexOf(char) > -1)
                {
                    let n = (alphal.indexOf(char) + 10)
                    result.push(`(+(${this.getInteger(n)}))[${this.words["toString"]}](${this.getInteger(36)})`)
                }
                else
                {
                    return "";
                }
            }
            return result.join("+");
        }
 
    }
Я думаю, вполне понятно, что в последней ветке, где сейчас return "";, должен быть универсальный код, который справится с кодированием любого символа. Есть несколько способов получения символа по его коду, из которых наиболее очевидным является использование метода fromCharCode, но, к сожалению, на данном этапе получить символ C в верхнем регистре - не представляется возможным. Так что пока мы оставим эту затею и перейдем к рассмотрению другого важного вопроса, без которого дальше мы не сможем двигаться.

Работа с функциями



До сего момента мы занимались исключительно созданием словаря для генерации текста и чисел. Но ведь наша задача состоит в том, чтобы исполнить код, да еще при этом и любой код. Понятно, что ключевые слова запрещены, поэтому единственным способом выполнения кода будет отправка текста программы на исполнение, то есть ровно то, что делает функция eval. На самом деле этот способ доступен нам уже сейчас, поскольку мы вполне можем сделать следующее:
Код javascript Выделить
([]["fill"]["constructor"]("alert(1)"))()
Можно выполнить этот код в браузере и он запустит alert(1), а мы передали его в виде текста. Таким образом уже сейчас понятно, что из себя представляет программа на JSFuck, это ровно то, что напсано выше, только вместо алерта перекодированный текст программы и именно поэтому мы столько времени уделили именно генерации текста, поскольку все остальное не представляет никакой сложности.

Рассмотрим, что здесь происходит.
[]["fill"] - возвращает ссылку на метод fill массива. Далее когда мы у него запрашиваем конструктор, мы получаем ссылку на Function, поскольку именна эта функция является в JavaScript конструктором всех функций вообще. Если мы вызываем эту функцию, передав ей в качестве аргумента строку с кодом, то она возвратит нам объект функции ровно с тем кодом, который мы передали. После этого функцию надо всего лишь вызвать.

Но для чего я пишу об этом сейчас, когда еще не получен инструмент, позволяющий генерировать любой текст? Все дело в том, что для получения некоторых символов нам понадобятся некоторые специфические объекты, доступ к которым мы сможем получить только через функции с собственным текстом. То есть функции мы будем использовать еще до того, как запустим код программы. Мало того, мы уже пользовались созданием фукнции, правда без кода, когда использовали конструкцию []+[]["fill"]["constructor"]() для получения слова anonymous.

Итак, наша текущая цель - получение буквы "C" для формирования имени фукнции fromCharCode, которая позволит нам генерировать любой текст. Получить этот символ нам поможет функция escape, как известно эта фукнция кодирует любой текст в URL-формат, то есть, если в текст присутствуют символы, недопустимые для URL она заменяет эти символы на знак процента, за которым следует код символа в шестнадцатиричной системе, то есть помимо цифр используются еще шесть букв латинского алфавита, причем в верхнем регистре и буква C как раз есть среди этих шести. Поэтому перед нами стоит задача - получить ссылку на эту функцию, а также найти какой-нибудь символ, который не разрешен URL-форматом и при этом его код в шестнадцатиричном формате содержит нужную нам букву.

Первая задача решается как раз при помощи создания функции, возвращающей нужную нам фукнцию
Код javascript Выделить
Function("return escape")
В качестве символа, отвечающего описанным выше условиям можно использовать <. А получить мы его можем с помощью одной из фукнций, оборачивающих строку в теги, таких как bold, italics и т. п.
Код javascript Выделить
(Function("return escape")())("".italics())
Данное выражение вернет нам строку "%3Ci%3E%3C/i%3E", в которой присутствует ряд новых символов, включая то что нам нужно. Таким образом мы можем дописать код метода encodeText так, чтобы он справлялся с любым текстом. Добавим в словарик еще пару элементов
Код javascript Выделить
    let fn = `([][${this.encodeText("fill")}][${constructor}])`;
    this.basics["<i></i>"] = `([]+[])[${this.encodeText("italics")}]()`;
    this.basics["%3Ci%3E%3C/i%3E"] = `(${fn}(${this.encodeText("return escape")}))()(${this.basics["<i></i>"]})`;
Последняя ветка encodeText теперь приобретет вид
Код javascript Выделить
    else
    {
        let stringCtor = `(([]+[])[${this.words.constructor}])`;
        let charcode = this.getInteger(char.charCodeAt(0));
        result.push(`${stringCtor}[${this.encodeText("fromCharCode")}](${charcode})`);
    }
Кроме того, теперь нашему классу мы уже можем добавить метод translate, который и будет выполнять первод кода JavaScript в формат JSFuck
Код javascript Выделить
    translate(jscode)
    {
        return `(([][${this.encodeText("fill")}][${this.encodeText("constructor")}])(${this.encodeText(jscode)}))()`;
    }
Теперь создадим страничку, на которой разместим наш транслятор, два текстовых поля, кнопку перевода, и кнопку запуска готового кода. Кроме того будем отображать длину полученного текста программы.
Код phphtml Выделить
<!DOCTYPE html>
 
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8" />
    <title></title>
</head>
<body>
    <textarea id="js" cols="70" rows="30">alert("hello")</textarea>
    <button onclick="document.getElementById('result').value = jsf.translate(document.getElementById('js').value);
    document.getElementById('charcount').textContent=document.getElementById('result').value.length">
        Encode
    </button>
    <textarea id="result" cols="70" rows="30"></textarea>
    <div id="charcount"></div>
    <button id="run" onclick="eval(document.getElementById('result').value)">Run</button>
    <script>
 
        class JSFuck
        {
            constructor()
            {
                this.basics = {
                    "true": "([]+!![])",
                    "false": "([]+![])",
                    "undefined": "([]+[][[]])"
                }
    
            }
 
            getDigit(d)
            {
                d = +d;
                return "[" + (d == 0 ? "+[]" : Array.apply(null, { length: d }).fill("+!+[]").join("")) + "]";
            }
            getInteger(n)
            {
                return n.toString().split("").map((v) => this.getDigit(+v)).join("+");
            }
 
            encodeText(text)
            {
                let alphal = "abcdefghijklmnopqrstuvwxyz", digits = "0123456789";
                let allChars = Object.keys(this.basics).join("").split("").filter((v, i, a) => a.indexOf(v) == i)
                let result = [];
                for (let char of text)
                {
                    if (allChars.includes(char))
                    {
                        for (let w in this.basics)
                        {
                            let index = w.split("").findIndex(c => c == char);
                            if (index > -1)
                            {
                                result.push(`(${"([]+" + this.basics[w] + ")"}[${this.getInteger(index)}])`);
                                break;
                            }
                        }
                    }
                    else if (digits.indexOf(char) > -1)
                    {
                        result.push(this.getDigit(char));
                    }
                    else if (alphal.indexOf(char) > -1)
                    {
                        let n = (alphal.indexOf(char) + 10)
                        result.push(`(+(${this.getInteger(n)}))[${this.words["toString"]}](${this.getInteger(36)})`)
                    }
                    else
                    {
                        let stringCtor = `(([]+[])[${this.words.constructor}])`;
                        let charcode = this.getInteger(char.charCodeAt(0));
                        result.push(`${stringCtor}[${this.encodeText("fromCharCode")}](${charcode})`);
                    }
                }
                return result.join("+");
            }
            translate(jscode)
            {
                return `(([][${this.encodeText("fill")}][${this.encodeText("constructor")}])(${this.encodeText(jscode)}))()`;
            }
        }
 
        var jsf = new JSFuck();
 
 
    </script>
</body>
</html>
Открыв эту страничку в браузере, можно перекодировать текст JavaScript в формат JSFuck. Таким образом поставленная цель достигнута. Полученный код имеет просто чудовищный размер и размер такой программы в любом случае будет большим, но, в то же время, это не значит, что его вообще нельзя оптимизировать. И хотя оптимизация не входила в задачи этого исследования, тем не менее, думаю, будет небезинтересно рассмотреть некоторые возможности. Кроме того, использовать функцию escape не рекомнедуется, а для получения encodeURI у нас есть не все буквы. Так что далее мы рассмотрим еще несколько второстепенных вопросов.

Использование encodeURI вместо escape



Поскольку функция escape является устаревшей и ее даже обещают в будущем удалить, а она у нас играет достаточно важную роль, неплохо было бы решить проблему ее замены. Конечно можно написать encodeURI вместо escape и вроде бы все готово, но проблема в том, что к тому моменту как мы использовали escape, у нас еще не было букв "URI". Стало быть, для того, чтобы можно было использовать эту функцию нам нужно откуда-то их взять, причем получить их по кодам символов мы не можем, если откажемся от использования escape. Дальше опишу откуда берутся эти буквы.
I можно получить из слова Infifnity
Код javascript Выделить
[]+(+"1e1000")
R можно получить из имени конструктора RegExp
(Function("return/false/"))()["constructor"]["name"]
С U дело немного сложнее, но совсем немного. Мы можем воспользоваться особенностями работы метода toString объекта {}. Если это просто объект, то он выводит его текстовое представление так [object Object] и здесь второе слово - это имя конструктора объекта, причем запсанное с большой буквы, независимо от того, с какой начинается реальное имя конструктора. Как он разрешает ситуации, когда объект не имеет свойств - мне неизвестно, но известно, что если этот метод вызывается для undefined, то результат будет таким [object Undefined], что, в принципе, нашу задачу решает, таким образом букву U мы получим так
Код javascript Выделить
(Function("return{}"))()["toString"]["call"](undefined)
Все это нужно добавить в словарь basics и после этого можно будет исопльзовать encodeURI
Код javascript Выделить
    this.basics["Infinity"] = "([]+(+([+!+[]]+" + this.encodeText("e") + "+[+!+[]]+[+[]]+[+[]]+[+[]]" + ")))";
    let rex = `(${fn})(${this.encodeText("return/a/")})`;
    this.basics["RegExp"] = `${rex}()[${constructor}][${name}]`;
    this.basics["[object Undefined]"] = `${fn}(${this.encodeText("return{}")})()[${tostring}][${this.encodeText("call")}]([][[]])`
    this.basics["%3Ci%3E%3C/i%3E"] = `(${fn}(${this.encodeText("return encodeURI")}))()(${this.basics["<i></i>"]})`;

Несколько мыслей по поводу оптимизации вывода



В самом начале статьи я уже упомянул о том, что не ставлю цели сделать что-то более оптимальное, чем то, что уже имеется в сети и таким образом цели оптимизации я не ставил. Главное - это разобраться в вопросе и получить работающий транслятор. Эта задача на данный момент выполнена и тем не менее, некоторые вопросы относительно путей оптимизации вывода, я тут рассмотрю. Сразу скажу, что под оптимизацией я понимаю количество выводимых символов. Количество символов в любом случае будет огромным, но все-таки можно что-то предпринять для того, чтобы хотя бы небольшая программа имела размер, с которым может справиться реальный браузер.

Итак, что мы имеем на данный момент. Откроем нашу страничку и консоль разработчика в браузере. На страничке экземпляр класса JSFuck создан под именем jsf. Попробуем ввести в консоль следующее
Код javascript Выделить
jsf.encodeText("Привет").length
У меня вывело 104661. То есть для того, чтобы закодировать слово "Привет" нужно 104661 символ. По-моему как-то многовато. А если вместо escape использовать encodeURI, то результат будет 157677. Результат увеличился в полтора раза из-за такого мелкого (казалось бы) изменения. Таким образом мы выяснили, что на результат можно существенно повлиять и есть смысл задуматься о том, откуда берутся все эти "тыщи".

Проведем еще эксперимент. При загрузке страницы в первом поле есть код alert("hello"). Если его перекодировать с помощью странички в том виде, в котором она представлена на выше, то получится код из 31843 символов. Теперь добавим в словарик basics всего одну дополнительную запись
Код javascript Выделить
this.basics["".link()] = `([]+[])[${this.encodeText("link")}]()`;
В результате при перекодировании мы получили код из 9303 символов. Нормальная такая экономия.

Причина таких удивительных метаморфоз в том, что весь код на JSFuck - это одно выражение. Здесь нет ни переменных, ни разделителей типа точки с запятой, запятой или новой строки. Таким образом, когда мы фомрировали строительные блоки для получения следующей буквы, в коде транслятора их можно было сохранить в словарик и использовать по мере надобности. Но при генерации кода, всякий раз когда тот или иной элемент понадобится весь его код вставляется в выходной текст полностью. Вспомним, как долго мы шли к тому, чтобы получить латинскую C в верхнем регистре. Все те элементы, которые мы использовали для ее нахождения будут присутствовать когда нам понадобится эта буква. В результате для ее получения нужно 4462 символа при использовании escape. Для того, чтобы получить ее же с помощью encodeURI код будет содержать все те коды, которые мы использовали для получения URI. Там в одном из вариантов у меня получалось больше десяти тысяч символов только для одной буквы. В слове fromCharCode их две, а все слово "стоит" 13010 символов (в упомянутом варианте с encodeURI их было больше 26000). И все эти символы будут вставляться в код всякий раз, когда нам нужно получить символ с помощью fromCharCode. Слово "Привет" написано кирилицей и для каждого символа вызывается fromCharCode. В примере с алертом присутствовали две кавычки. В первом варианте для них вызывался fromCharCode (дважды), а когда в словарь была добавлена запись "".link(), которая содержит кавычки, код получения кавычки стал намного короче, что незамедлительно сказалось на объеме вывода.

Таким образом, по крайней мере одно направление оптимизации мы уже нашли: максимально сократить количество вызовов fromCharCode. Для этого желательно найти максимум возможностей получать символы коротким путем. Здесь нам может помочь тот самый полный листинг....

Правда логику поиска символа в словаре тоже немного придется изменить. Опять-таки маленький пример. Вот код инициализации словарей, который мы используем сейчас.
Код javascript Выделить
    this.basics["" + [].fill] = `([]+[][${this.encodeText("fill")}])`;
    this.basics["" + [].fill.constructor()] = `([]+[][${this.encodeText("fill")}][${this.encodeText("constructor")}]())`;
    this.words = {};
    let constructor = this.words.constructor = this.encodeText("constructor");
    let name = this.words.name = this.encodeText("name");
    let tostring = this.words["toString"] = this.encodeText("to") + `+(([]+[])[${constructor}][${name}])`
    let fn = `([][${this.encodeText("fill")}][${constructor}])`;
    this.basics["<i></i>"] = `([]+[])[${this.encodeText("italics")}]()`;
    this.basics["%3Ci%3E%3C/i%3E"] = `(${fn}(${this.encodeText("return escape")}))()(${this.basics["<i></i>"]})`;
    this.basics["".link()] = `([]+[])[${this.encodeText("link")}]()`;
Для получения латинского символа C требуется 4462 символа. Добавим в словарь записи, которые мы использовали для получения символов URI (именно добавим).
Код javascript Выделить
    this.basics["" + [].fill] = `([]+[][${this.encodeText("fill")}])`;
    this.basics["" + [].fill.constructor()] = `([]+[][${this.encodeText("fill")}][${this.encodeText("constructor")}]())`;
    this.words = {};
    let constructor = this.words.constructor = this.encodeText("constructor");
    let name = this.words.name = this.encodeText("name");
    let tostring = this.words["toString"] = this.encodeText("to") + `+(([]+[])[${constructor}][${name}])`
    let fn = `([][${this.encodeText("fill")}][${constructor}])`;
    this.basics["<i></i>"] = `([]+[])[${this.encodeText("italics")}]()`;
    this.basics["Infinity"] = "([]+(+([+!+[]]+" + this.encodeText("e") + "+[+!+[]]+[+[]]+[+[]]+[+[]]" + ")))";
    let rex = `(${fn})(${this.encodeText("return/a/")})`;
    this.basics["RegExp"] = `${rex}()[${constructor}][${name}]`;
    this.basics["[object Undefined]"] = `${fn}(${this.encodeText("return{}")})()[${tostring}][${this.encodeText("call")}]([][[]])`
    this.basics["%3Ci%3E%3C/i%3E"] = `(${fn}(${this.encodeText("return escape")}))()(${this.basics["<i></i>"]})`;
    this.basics["".link()] = `([]+[])[${this.encodeText("link")}]()`;
В результате тот же символ нам обойдется в 6247 символов выходного потока. Причину причин может быть две:
1. При поиске символва в словаре метод encodeText на длину не обращает внимания. Он находит самый певрый ключ словаря, в котором содержится нужный символ и использует именно этот ключ. Так что нужно либо отсортировать ключи в порядке возрастания длины значения, либо составить новый словарь, в котором каждому символу будет соответствовать наиболее короткий код, из тех, что имеется в словаре. Отсортировать ключи словаря можно, добавив в конце конструктора следующий код
Код javascript Выделить
                let sortedBasics = {},
                    keys = Object.keys(this.basics).sort((k1, k2) => this.basics[k1].length - this.basics[k2].length);
                for (let key of keys)
                {
                    sortedBasics[key] = this.basics[key];
                }
                this.basics = sortedBasics;
2. Среди добавленных в словарь ключей, есть такие, которые содержат буквы, которые лучше получать не из словаря, а с помощью преобразования чисел в 36-ричную систему счисления. Но поскольку сначала мы проверяем наличие символа в словаре, а перевод в 36-СС используем только если символ в словаре не найден, то имеем то, что имеем. Таким образом, наиболее разумным способом будет - найти символ разными способомами и выбрать самый короткий.
Код javascript Выделить
            encodeText(text)
            {
                let alphal = "abcdefghijklmnopqrstuvwxyz", digits = "0123456789";
                let allChars = Object.keys(this.basics).join("").split("").filter((v, i, a) => a.indexOf(v) == i).join("");
                let result = [];
                for (let char of text)
                {
                    if ((alphal + allChars).includes(char))
                    {
 
                        let r1 = "", r2 = "";
                        if (allChars.includes(char))
                        {
                            for (let w in this.basics)
                            {
                                let index = w.indexOf(char);
                                if (index > -1)
                                {
                                    r1 = `(([]+${this.basics[w]})[${this.getInteger(index)}])`;
                                    break;
                                }
                            }
                        }
                        if ("tostring" in this.words && alphal.includes(char))
                        {
                            let n = (alphal.indexOf(char) + 10)
                            r2 = (`(+(${this.getInteger(n)}))[${this.words["tostring"]}](${this.getInteger(36)})`)
                        }
                        result.push(((r1.length < r2.length && r1.length > 0) || r2.length === 0) ? r1 : r2);
                    }
                    else if (digits.indexOf(char) > -1)
                    {
                        result.push(this.getDigit(char));
                    }
                    else
                    {
                        let stringCtor = `(([]+[])[${this.words.constructor}])`;
                        let charcode = this.getInteger(char.charCodeAt(0));
                        result.push(`${stringCtor}[${this.encodeText("fromCharCode")}](${charcode})`);
                    }
                }
                return result.join("+");
            }

Но главная проблема, конечно же, в том, что для перекодирования произвольного текста нам приходится для каждого символа использовать fromCharCode, который нам так дорого достался. И если эта проблема не будет решена, то все описанные выше оптимизации - не более чем "экономия на спичках". Поэтому сосредоточится нужно на решении именно этой проблемы.

Оптимизация работы с fromCharCode



Мы уже видели, что вызов fromCharCode обходится довольно дорого и если перекодировать длинный текст, в котором достаточно много букв будет вызываться этим методом, то объем выходного кода может оказаться в принципе непригодным даже для демонстрации. Поэтому нам нужно подумать, как избежать такого частого вызова этого метода.

Сам метод принимает любое количество аргументов и может за один вызов перекодировать достаточно длинный текст. Казалось бы: вот и решение. Но, не тут-то было. Дело в том, что, имея в распоряжении только шесть символов, мы не так много и можем сделать, по крайней мере, если действовать напрямую. Аргументы фукнции разделяются запятыми, а запятой у нас нет в арсенале. Можно было бы вспомнить такую замечательную штуку, как apply - метод, который принимает аргументы функции, для которой он вызывается, в виде массива. Но проблема в том, что массив аргументов - это второй аргумент apply, а стало быть передать его можно только через запятую. В современных браузерах можно использовать три точки перед массивом, но у нас и точек нет.

Выходом из нашей ситуации может быть создание собственной функции, которая принимала бы массив кодов и возвращала текст, составленный из символов, соответствующих полученным кодам. Правда тут мы сталкиваемся с еще одной той же самой проблемой. Для того, чтобы функция, созданная с помощью Function могла принимать аргументы, конструктор функций сам должен быть вызван с двумя аргументами Function(arglist, code), а если бы у нас была такая возможность, то к этому приему мы вообще не стали бы обращаться.
Тем не менее, возможности языка позволяют нам создать нужную функцию, благодаря коллекции arguments, из которой мы можем получить аргументы функции, даже если она была объявлена без параметров. Таким образом нам нужно создать функцию со следующим кодом.
Код javascript Выделить
return String.fromCharCode.apply(false,arguments[0])
То есть если мы теперь сделаем вот так
Код javascript Выделить
(Function("return String.fromCharCode.apply(false,arguments[0])"))([1055, 1088, 1080, 1074, 1077, 1090])
То такое выражение, возвратит нам слово "Привет", поскольку коды букв именно этого слова были переданы нашей функции. Но тут возникает другой вопрос: а как нам создать массив, ведь там тоже есть запятые и это тоже не запятые в тексте, которые мы вполне можем создать, а именно запятые, как часть кода? Но, на самом деле, эта проблема как раз решается элементарно, а именно - с помощью метода split. Конечно, в результате получится массив строк, но JavaScript легко конвертирует их в числа при передаче функции, требующей числовых аргументов, в этом несложно убедиться, выполнив предыдущий код с маленькими изменениями.
Код javascript Выделить
(Function("return String.fromCharCode.apply(false,arguments[0])"))(["1055", "1088", "1080", "1074", "1077", "1090"])
Таким образом, нам надо выбрать что-то в качестве разделителя и перекодировать текст. Я выбрал в качестве разделителя слово false, поскольку оно может быть закодировано тремя символами и еще потребуется по два плюса, то есть, для кодирования слова привет нам нужно будет сделать следующее
Код javascript Выделить
(Function("return String.fromCharCode.apply(false,arguments[0])"))(("1055"+![]+"1088"+![]+"1080"+![]+"1074"+![]+"1077"+![]+"1090")["split"](![]))
Получив текст таким образом, его надо будет передать на выполнение тем же способом, что и раньше. Добавим в наш класс новый метод translateLarge
На нашу страничку мы добавим еще одну кнопку, для того, чтобы можно было кодировать текст уже с помощью нового метода. И сможем сравнить что получаем в том и другом случае.
Окончательная версия нашей странички для перевода текста имеет следующий вид.
Код phphtml Выделить
<!DOCTYPE html>
 
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8" />
    <title></title>
</head>
<body>
    <textarea id="js" cols="70" rows="30">alert("hello")</textarea>
    <button onclick="document.getElementById('result').value = jsf.translate(document.getElementById('js').value);
    document.getElementById('charcount').textContent=document.getElementById('result').value.length">
        Encode
    </button>
    <textarea id="result" cols="70" rows="30"></textarea>
    <div id="charcount"></div>
    <button id="runlarge" onclick="eval(document.getElementById('result').value)">Run</button>
    <button onclick="document.getElementById('result').value = jsf.translateLarge(document.getElementById('js').value);
    document.getElementById('charcount').textContent=document.getElementById('result').value.length">
        EncodeLarge
    </button>
    <script>
        class JSFuck
        {
            constructor()
            {
                this.basics = {
                    "true": "([]+!![])",
                    "false": "([]+![])",
                    "undefined": "([]+[][[]])"
                }
                this.words = {};
                this.basics["" + [].fill] = `([]+[][${this.encodeText("fill")}])`;
                let constructor = this.words["constructor"] = this.encodeText("constructor");
                this.basics["" + [].fill.constructor()] = `([]+[][${this.encodeText("fill")}][${constructor}]())`;
                let name = this.encodeText("name");
                this.words.name = name;
                let tostring = this.encodeText("to") + `+(([]+[])[${constructor}][${name}])`
                this.words["tostring"] = tostring;
                let fn = `([][${this.encodeText("fill")}][${constructor}])`;
                this.basics["<i></i>"] = `([]+[])[${this.encodeText("italics")}]()`;
                this.basics["Infinity"] = "([]+(+([+!+[]]+" + this.encodeText("e") + "+[+!+[]]+[+[]]+[+[]]+[+[]]" + ")))";
                let rex = `(${fn})(${this.encodeText("return/a/")})`;
                this.basics["RegExp"] = `${rex}()[${constructor}][${name}]`;
                this.basics["[object Undefined]"] = `${fn}(${this.encodeText("return{}")})()[${tostring}][${this.encodeText("call")}]([][[]])`
                this.basics["%3Ci%3E%3C/i%3E"] = `(${fn}(${this.encodeText("return escape")}))()(${this.basics["<i></i>"]})`;
                this.basics["".link()] = `([]+[])[${this.encodeText("link")}]()`;
                //this.basics[","] = `[[]][${this.encodeText("concat")}]([[]])+[]`;
                let sortedBasics = {},
                    keys = Object.keys(this.basics).sort((k1, k2) => this.basics[k1].length - this.basics[k2].length);
                for (let key of keys)
                {
                    sortedBasics[key] = this.basics[key];
                }
                this.basics = sortedBasics;
            }
 
            getDigit(d)
            {
                d = +d;
                return "[" + (d == 0 ? "+[]" : Array.apply(null, { length: d }).fill("+!+[]").join("")) + "]";
            }
            getInteger(n)
            {
                return n.toString().split("").map((v) => this.getDigit(+v)).join("+");
            }
 
            encodeText(text)
            {
                let alphal = "abcdefghijklmnopqrstuvwxyz", digits = "0123456789";
                let allChars = Object.keys(this.basics).join("").split("").filter((v, i, a) => a.indexOf(v) == i).join("");
                let result = [];
                for (let char of text)
                {
                    if ((alphal + allChars).includes(char))
                    {
 
                        let r1 = "", r2 = "";
                        if (allChars.includes(char))
                        {
                            for (let w in this.basics)
                            {
                                let index = w.indexOf(char);
                                if (index > -1)
                                {
                                    r1 = `(([]+${this.basics[w]})[${this.getInteger(index)}])`;
                                    break;
                                }
                            }
                        }
                        if ("tostring" in this.words && alphal.includes(char))
                        {
                            let n = (alphal.indexOf(char) + 10)
                            r2 = (`(+(${this.getInteger(n)}))[${this.words["tostring"]}](${this.getInteger(36)})`)
                        }
                        result.push(((r1.length < r2.length && r1.length > 0) || r2.length === 0) ? r1 : r2);
                    }
                    else if (digits.indexOf(char) > -1)
                    {
                        result.push(this.getDigit(char));
                    }
                    else
                    {
                        let stringCtor = `(([]+[])[${this.words.constructor}])`;
                        let charcode = this.getInteger(char.charCodeAt(0));
                        result.push(`${stringCtor}[${this.encodeText("fromCharCode")}](${charcode})`);
                    }
                }
                return result.join("+");
            }
            translate(jscode)
            {
                return `(([][${this.encodeText("fill")}][${this.encodeText("constructor")}])(${this.encodeText(jscode)}))()`;
            }
 
            translateLarge(jscode)
            {
                let fncode = this.encodeText("return String.fromCharCode.apply(false,arguments[0])");
                let fn = `([][${this.encodeText("fill")}][${this.encodeText("constructor")}])`;
                let ccfn = `((${fn})(${fncode}))`
                let charCodes = jscode.split("").map(c => this.getInteger(c.charCodeAt(0))).join("+![]+");
                let codeArray = `(${charCodes})[${this.encodeText("split")}](![])`;
                let code = `${ccfn}(${codeArray})`;
                return `(([][${this.encodeText("fill")}][${this.encodeText("constructor")}])(${code}))()`;
            }
        }
 
        var jsf = new JSFuck();
 
    </script>
</body>
</html>

Немного тестов



Теперь, когда код написан, будет совсем не лишним посмотреть объем того, что выдает старый код, новый код(оптимизированный) и код на странице Мартина Клеппе JSFuck - Write any JavaScript with 6 Characters: []()!+
Я буду отображать три числа в следующем порядке:
1. Количество символов, генерируемое вышеуказанной страницей
2. Количество символов, генерируемое первоначальным кодом(кнопка Encode)
3. Количество символов, генерируемое оптимизированным кодом (кнопка EncodeLarge)

Сначала пробуем код alert(1)
1227 1536 85579
Прямо скажем: результат результат не очень, причем неоптимизированный код как-то худо-бедно близок к тому, что выдает гуру JSFuck, а оптимизированный выдает что-то запредельное.
Но не будем останавливаться на этом простом примере и пойдем дальше. Проверим код
Код javascript Выделить
var x="";for(let i=0;i<10;i++)x+=i*i;alert(x)
Результат:
53113 133965 87381
Здесь код Мартина Клеппе опять выдал наиболее короткий вариант, а вот в моих кодах уже есть некоторые изменения. А именно: оптимизированный вариант уже начал оправдывать то, что я называю его оптимизированным. При этом следует заметить, что по сравнения с предыдущим кодом, размер выходного текста изменился но не сильно( это важно).

Теперь попробуем закинуть туда что-нибудь подлиннее. Например код нашего класса JSFuck целиком. И вот какие результаты мы получим

3673690 6791169 310945

И оказалось, что оптимизированный транслятор выдал результат более чем в десять раз короткий по сравнению с транслятором Мартина Клеппе, а сравнение с моим неоптимизированным кодом еще более внушительное.

Объяснение всему этому достаточно простое. Мы достаточно много символов потратили на то, чтобы создать удобную функцию, позволяющую транслировать коды символов. Весь этот инструментарий генерирует порядка 85 тыс. символов. Поэтому, каким бы коротким ни был код, результат при использовании этого метода не может быть меньше примерно этого числа. Зато все остальные символы, независимо от того, что это за символы, будут "весить" относительно немного.

Комментариев нет :

Отправить комментарий