- О чем пойдет речь
- Базовые понятия
- Приступим к написанию кода
- Дополним базовый словарь
- Работа с функциями
- Использование encodeURI вместо escape
- Несколько мыслей по поводу оптимизации вывода
- Оптимизация работы с fromCharCode
- Немного тестов
О чем пойдет речь
Для начала цитата из Википедии
Цитата:
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.
[
,]
, (
, )
, !
, и +
.Имя произведено от Brainfuck, эзотерического языка программирования, который также использует минималистический алфавит, состоящий исключительно из знаков пунктуации. В отличие от Brainfuck, для которого требуется собственный компилятор или интерпретатор, JSFuck является валидным кодом JavaScript, что подразумевает, что JSFuck-программа может быть запущена в каком-либо браузере или движке, который интерпретирует JavaScript.
Существует ресурс 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": "([]+[][[]])"
}
}
}
Код javascript | Выделить |
getDigit(d)
{
d = +d;
return "[" + (d == 0 ? "+[]" : Array.apply(null, { length: d }).fill("+!+[]").join("")) + "]";
}
Код 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")}])`;
}
"" + [].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}])`
Код javascript | Выделить |
this.basics["String"] = `+(([]+[])[${constructor}][${name}])`;
Код javascript | Выделить |
(10)["toString"](36) // a
(11)["toString"](36) // b
....................
(34)["toString"](36) // y
(35)["toString"](36) // z
Код 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())
Код 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>"]})`;
Код javascript | Выделить |
else
{
let stringCtor = `(([]+[])[${this.words.constructor}])`;
let charcode = this.getInteger(char.charCodeAt(0));
result.push(`${stringCtor}[${this.encodeText("fromCharCode")}](${charcode})`);
}
Код 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>
Использование encodeURI вместо escape
Поскольку функция escape является устаревшей и ее даже обещают в будущем удалить, а она у нас играет достаточно важную роль, неплохо было бы решить проблему ее замены. Конечно можно написать encodeURI вместо escape и вроде бы все готово, но проблема в том, что к тому моменту как мы использовали escape, у нас еще не было букв "URI". Стало быть, для того, чтобы можно было использовать эту функцию нам нужно откуда-то их взять, причем получить их по кодам символов мы не можем, если откажемся от использования escape. Дальше опишу откуда берутся эти буквы.
I можно получить из слова Infifnity
Код javascript | Выделить |
[]+(+"1e1000")
(Function("return/false/"))()["constructor"]["name"]
С U дело немного сложнее, но совсем немного. Мы можем воспользоваться особенностями работы метода toString объекта {}. Если это просто объект, то он выводит его текстовое представление так
[object Object]
и здесь второе слово - это имя конструктора объекта, причем запсанное с большой буквы, независимо от того, с какой начинается реальное имя конструктора. Как он разрешает ситуации, когда объект не имеет свойств - мне неизвестно, но известно, что если этот метод вызывается для undefined, то результат будет таким [object Undefined]
, что, в принципе, нашу задачу решает, таким образом букву U мы получим такКод javascript | Выделить |
(Function("return{}"))()["toString"]["call"](undefined)
Код 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
Проведем еще эксперимент. При загрузке страницы в первом поле есть код
alert("hello")
. Если его перекодировать с помощью странички в том виде, в котором она представлена на выше, то получится код из 31843 символов. Теперь добавим в словарик basics всего одну дополнительную записьКод javascript | Выделить |
this.basics["".link()] = `([]+[])[${this.encodeText("link")}]()`;
Причина таких удивительных метаморфоз в том, что весь код на 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")}]()`;
Код 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")}]()`;
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;
Код 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])
Код javascript | Выделить |
(Function("return String.fromCharCode.apply(false,arguments[0])"))(["1055", "1088", "1080", "1074", "1077", "1090"])
Код javascript | Выделить |
(Function("return String.fromCharCode.apply(false,arguments[0])"))(("1055"+![]+"1088"+![]+"1080"+![]+"1074"+![]+"1077"+![]+"1090")["split"](![]))
На нашу страничку мы добавим еще одну кнопку, для того, чтобы можно было кодировать текст уже с помощью нового метода. И сможем сравнить что получаем в том и другом случае.
Окончательная версия нашей странички для перевода текста имеет следующий вид.
Код 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 тыс. символов. Поэтому, каким бы коротким ни был код, результат при использовании этого метода не может быть меньше примерно этого числа. Зато все остальные символы, независимо от того, что это за символы, будут "весить" относительно немного.
Комментариев нет :
Отправить комментарий