Javascript – это странный и прекрасный язык, который позволяет писать безумный, но все еще валидный код. Он пытается помочь нам, конвертируя одни штуки в другие в зависимости от того, как мы работаем с ними.
Если добавить строку к чему-то, то он допустит, что мы хотим получить текст, поэтому сконвертирует все в строку.
Если мы добавляем префикс "плюс" или "минус", то он допустит, что нам нужно числовое представление и сконвертирует строку в число, если сможет.
Если мы отрицаем что-то, то он сконвертирует это в булево значение.
Мы можем использовать эти особенности языка и создать немного магии со всего-лишь шестью символами: [
,]
,(
,)
,!
и +
. Если вы читаете это на десктопе, то можете открыть консоль в вашем браузере (developer tools, например) и запускать код. Просто копируйте любой код из примеров ниже в консоль, и он должен исполнится и вернуть true.
Давайте начнем с простого. Вот главные правила:
- Префикс
!
конвертирует в Boolean - Префикс
+
конвертирует в Number - Добавление
[]
конвертирует String
Вот они в действии:
![] === false
+[] === 0
[]+[] === ""
Еще один важный момент, о котором стоить помнить — с помощью квадратных скобок можно получать конкретный символ (букву) из строки вот так:
"hello"[0] === "h"
Также помните, что можно брать несколько цифр и складывать их в строковом представлении, а потом конвертировать это обратно в число.
+("1" + "1") === 11
Хорошо, давайте попробуем скомбинировать эти трюки и получить букву a
.
![] === false
![]+[] === "false"
+!![] === 1
------------------------
(![]+[])[+!![]] === "a" // same as "false"[1]
Прикольно!
Этой довольно простой комбинацией можно получить все буквы из слов true
и false
. a
,e
,f
,l
,r
,s
,t
,u
. Окей, можно ли получить буквы еще откуда-нибудь?
Ну, есть undefined
, который можно получить с помощью странной ерунды вроде [][[]]
. Сконвертируем его в строку используя одно из наших главных правил и получим дополнительные буквы d
,i
and n
.
[][[]] + [] === "undefined"
С помощью букв, которые у нас пока есть, можно написать слова fill
, filter
и find
. Конечно, есть и другие слова, но нам интересны именно эти три слова, потому что это методы массива (Array). То есть они являются частью объекта Array
и их можно вызывать напрямую у экземпляра массива. Например, [2,1].sort()
.
Важная особенность Javascript: свойства объекта доступны через точку (dot notation) или квадратные скобки (square bracket notation). Так как методы массива — это свойства самого объекта Array, можно вызвать эти методы с помощью квадратных скобок вместо точки.
То есть [2,1]["sort"]()
- это то же самое, что [2,1].sort()
.
Давайте посмотрим, что будет если использовать один из методов массива с помощью доступных букв, но не будем вызывать его:
[]["fill"]
Это дает function fill() { [native code] }
. Можно сконвертировать этот заголовок метода в строку известным нам правилом:
[]["fill"]+[] === "function fill() { [native code] }"
Вот, теперь у нас есть дополнительные символы: c
,o
,v
,(
,)
,{
,[
,]
,}
,?
.
С помощью букв c
и o
мы теперь можем написать constructor
. constructor
- это метод, доступный во всех объектах Javascript, он просто возвращает функцию-конструктор.
Давайте получим строковое представление функции-конструктора для всех доступных нам сейчас объектов:
true["constructor"] + [] === "function Boolean() { [native code] }"
0["constructor"] + [] === "function Number() { [native code] }"
""["constructor"] + [] === "function String() { [native code] }"
[]["constructor"] + [] === "function Array() { [native code] }"
Отсюда мы получаем новые буквы для арсенала: B
,N
,S
,A
,m
,g
,y
.
Теперь можно собрать слово "toString"
. Это функция, которую можно вызывать с квадратными скобками. Да, на этот раз мы на самом деле вызовем ее:
(10)["toString"]() === "10"
Но мы ведь и так могли конвертировать все что угодно в строку с помощью одного из основных правил. В чем польза?
Ну, а что если я скажу, что метод toString
у типа Number
обладает секретным аргументом под названием radix
, который меняет основание системы счисления возвращаемого числа перед конвертацией в строку. Смотрите:
(12)["toString"](10) === "12" // основание 10
(12)["toString"](2) === "1100" // основание 2, бинарная система
(12)["toString"](8) === "14" // основание 8, восьмеричная система
(12)["toString"](16) === "c" // шестнадцатеричная система 12
Но зачем останавливаться на 16? Максимум это 36, что включаем в себя все цифры 0
-9
и буквы a
-z
. Теперь можно получить любой символ:
(10)["toString"](36) === "a"
(35)["toString"](36) === "z"
Круто! Но что делать с другими символами вроде знаком препинания и заглавными буквами? Ныряем еще глубже в кроличью нору!
В зависимости от того, где вы запускаете Javascript, у вас может быть или не быть доступ к некоторым pre-defined объектам и данным. Если вы работаете в браузере, то вам скорее всего доступны оберточные методы HTML.
Например, bold
- это метод строки, который добавляет теги для полужирности:
"test"["bold"]() === "<b>test</b>"
Отсюда можно достать <
, >
и /
.
Вы, наверное, слышали про функцию escape
. Она, грубо говоря, конвертирует строку в формат URI, чтобы простые браузеры могли интерпретировать ее. Если передать ей пробел, то получим %20
. Если передать ей <
, то получим %3C
. Эта заглавная C
очень важна если нужно получить все оставшиеся недостающие символы.
С помощью этой буквы можно написать fromCharCode
. Эта функция возвращает символ Юникода на основе заданного десятеричного числа. Она – часть объекта String, который можно получить вызовом конструктора, как мы уже делали раньше.
""["constructor"]["fromCharCode"](65) === "A"
""["constructor"]["fromCharCode"](46) === "."
Можно использовать Unicode lookup и с легкостью найти код для любого символа Юникода.
Так, теперь мы можем написать что угодно в виде строки, и можем запустить любую функцию у типов Array, String, Number, Boolean и Object через их конструкторы. Приличная мощь для всего лишь шести символов. Но это еще не все.
Что такое конструктор любой функции?
Ответ это function Function() { [native code] }
, то есть сам объект Function.
[]["fill"]["constructor"] === Function
Используя это можно передать строку кода и создать настоящую функцию.
Function("alert('test')");
Получаем:
Function anonymous() {
alert('test')
}
И ее можно сразу же вызывать добавив ()
в конец. Да, теперь мы можем запускать настоящий код!
Уфф! Все!
Теперь у нас есть доступ ко всем символам, можно писать ими любой код и запускать его. Так что Javascript Тьюринг-полный со всего лишь шестью символами [
,]
,(
,)
,+
и !
.
Хотите доказательств? Запустите этот код в консоли.
Есть инструмент, который автоматизирует конвертацию, и вот как он переводит каждый символ.
В чем польза?
Ни в чем. eBay делал нехорошие штуки еще совсем недавно, что позволяло продавцам вставлять исполняемый JS в свои страницы используя эти символы, но это не совсем типичный вектор атаки. Некоторые люди поднимают тему обфускации, но, честно говоря, для этого есть методы получше.
Извините.
Но, надеюсь, вам понравилось это путешествие.
Источники:
Комментарии (28)
trik
10.10.2016 11:15Некоторые люди поднимают тему обфускации, но, честно говоря, для этого есть методы получше.
С этого момента можно подробнее?LoadRunner
10.10.2016 11:19base64, использование замен символов на их \x и \u коды всё ещё неплохо обфусцирует.
gearbox
10.10.2016 11:26+1Нет в js нормальной обфускации и быть не может по определению. Вот здесь: https://jsplumbtoolkit.com/ ребята защищают свой продукт (не дешевый ни разу) похожими техниками (как в статье). Толку? У меня один вечер ушел на то чтобы отвязать их продукт от домена (не корысти ради, реально интересно было).
sanex3339
10.10.2016 13:26Отвязка от домена это в любом случае нахождение куска кода, который проверяет этот самый домен с зашифрованными внутри себя разрешенными доменами и удаление этого куска. Зная, что такой код должен выполняться как можно раньше, то найти такой кусок довольно просто.
Основная цель обфускации — максимально усложнить понимание логики кода. С этой задачей обфускаторы справляются отлично. Речь, разумеется, о объеме кода 500+ строк.
Например, что делает этот код?
https://gist.github.com/sanex3339/556d5e66ec787f16a6a0fb4b5fd729f6gearbox
10.10.2016 15:02+2Вы верно шутите? И судя по всему код по моей ссылке не смотрели. Что касается Вашего упражнения — каждый вызов функции заменяем на toString() и смотрим исходник «обфусцированных» функций.
sanex3339
10.10.2016 15:20В результате .toString() вышеуказанного кода вы получите такой же обфусцированный код вида
function _0x1d7251(_0x1daaeb) { function _0x389dc4() { console[_0x19d4('0x0')](_0x19d4('0x1'), _0x1daaeb[_0x19d4('0x2')]); } console[_0x19d4('0x0')](_0x19d4('0x3'), _0x1daaeb); var _0x1daaeb = {}; return _0x1daaeb[_0x19d4('0x2')] = 0xf, _0x389dc4(); }
gearbox
10.10.2016 15:42+1И? проблема вывести в консоль кто что значит?
Вы действительно думаете что вот это:
var _0x91f14c=[]['\x66\x69\x6c\x74\x65\x72']['\x63\x6f\x6e\x73\x74\x72\x75\x63\x74\x6f\x72']('\x72\x65\x74\x75\x72\x6e\x20\x74\x68\x69\x73')();
может кого то остановить? тут вручную можно пройтись и получить:
var _0x91f14c=[].filter.constructor('\x72\x65\x74\x75\x72\x6e\x20\x74\x68\x69\x73')(); //this
И так далее по списку.
если текста РЕАЛЬНО много — можно автоматизировать процесс, парсеров хватает.
Hauts
10.10.2016 21:08http://www.jsnice.org помогает, хотя в логику особо и не всматривался. Я так понимаю, там что-то связанное с (де)кодированием в base64 и почему-то метод с названием html5 валяется.
romanonthego
10.10.2016 12:05одна из тех вещей которые прикольные в теории, но если вы используете их в реальной жизни — с вами серьезно что-то не так…
saboteur_kiev
10.10.2016 14:27+(«1» + «1») === 11
Вот тут я не очень понял, почему не 12?
Ну то есть я вижу, что в девелопере все верно, но можно пояснить, ведь интуитивно — инкрементация должна была произойти до сравнения?saboteur_kiev
10.10.2016 14:31Хм. вроде сам понял. Инкрементация это если ++
А тут «ничего» добавляется к «11» и получается 11.
Aingis
10.10.2016 14:34+1Потому что складываются строки. Если хотя бы один из операндов строка, делается приведение к строке. Унарный плюс просто приводит к числу, короткий аналог
Number()
.
"1" + "1" // "11" +("11") // 11
trapwalker
10.10.2016 15:02+1Этот сабсет, ИМХО, гораздо полезнее брейнфака. На правах петросяна. Если жизнь и самозародится когда-то в недрах интернета, то как-то вот так вот.
Grammka
10.10.2016 15:26+1Бывает очень весело =)
{}[0] == 0 && 0 != {}[0] // true
Aingis
10.10.2016 16:05+1Внезапно
({}[0] == 0 && 0 != {}[0]) // false ({}[0] == 0) // false
funca
11.10.2016 09:39+1вообще-то
все простоЭто особенность консоли (или функции eval) — eval интерпретирует фигурные скобки не как объект, а как скобочное выражение. eval("{}") -> undefined
"{}[0]" в начале строки интерпретируется как скобочное выражение за которым следует массив (примерно то же самое как написать ";[0]" ). Ну а то, что [0] == 0 и так все знают. А еще eval возвращает значение последнего выражения. Можно было написать 0 == 0 или ;0 == 0 или {}0 == 0, что тоже выглядит странно, но уже не так эффектно.
В правой части {}[0] это нормальный statement, где из пустого объекта пытаются получить свойство 0, результатом которого является, естественно, undefined. Естественно, что 0 != undefined.
Добавление круглых скобок снаружи превращает все выражение в statement, поэтому вычисляется как положено. eval("({})") -> Object
lasthand
11.10.2016 10:29+[] === +![]
По-прежнему забавно. Для ToNumber "[]" проходит через ToPrimitive, который пуст. А для ToBoolean "[]" это объект, который всегда true.
Large
12.10.2016 12:52Какое-то дежавю, это ведь все уже было на хабре, недавно еще и статья про цикад и цсс начала снова появляться в ленте.
ripatti
Сразу же вспомнил про эту задачу https://ipsc.ksp.sk/2015/real/problems/m.html.
Правда, оказалось, там были чуть другие 6 символов.