Javascript-путешествие с шестью символами

Original author: Jasper Cashmore
  • Translation


Javascript – это странный и прекрасный язык, который позволяет писать безумный, но все еще валидный код. Он пытается помочь нам, конвертируя одни штуки в другие в зависимости от того, как мы работаем с ними.


Если добавить строку к чему-то, то он допустит, что мы хотим получить текст, поэтому сконвертирует все в строку.


Если мы добавляем префикс "плюс" или "минус", то он допустит, что нам нужно числовое представление и сконвертирует строку в число, если сможет.


Если мы отрицаем что-то, то он сконвертирует это в булево значение.


Мы можем использовать эти особенности языка и создать немного магии со всего-лишь шестью символами: [,],(,),! и +. Если вы читаете это на десктопе, то можете открыть консоль в вашем браузере (developer tools, например) и запускать код. Просто копируйте любой код из примеров ниже в консоль, и он должен исполнится и вернуть true.


Давайте начнем с простого. Вот главные правила:


  1. Префикс ! конвертирует в Boolean
  2. Префикс + конвертирует в Number
  3. Добавление [] конвертирует String

Вот они в действии:


![] === false
+[] === 0
[]+[] === ""

Еще один важный момент, о котором стоить помнить — с помощью квадратных скобок можно получать конкретный символ (букву) из строки вот так:


"hello"[0] === "h"

Также помните, что можно брать несколько цифр и складывать их в строковом представлении, а потом конвертировать это обратно в число.


+("1" + "1") === 11

Хорошо, давайте попробуем скомбинировать эти трюки и получить букву a.


![] === false
![]+[] === "false"
+!![] === 1
------------------------
(![]+[])[+!![]] === "a"  // same as "false"[1]

Прикольно!


Этой довольно простой комбинацией можно получить все буквы из слов true и falsea,e,f,l,r,s,t,u. Окей, можно ли получить буквы еще откуда-нибудь?


Ну, есть undefined, который можно получить с помощью странной ерунды вроде [][[]]. Сконвертируем его в строку используя одно из наших главных правил и получим дополнительные буквы d,i and n.


[][[]] + [] === "undefined"

С помощью букв, которые у нас пока есть, можно написать слова fillfilter и 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 мы теперь можем написать constructorconstructor - это метод, доступный во всех объектах 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 в свои страницы используя эти символы, но это не совсем типичный вектор атаки. Некоторые люди поднимают тему обфускации, но, честно говоря, для этого есть методы получше.


Извините.


Но, надеюсь, вам понравилось это путешествие.


Источники:


Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 28

    +2
    Сразу же вспомнил про эту задачу https://ipsc.ksp.sk/2015/real/problems/m.html.
    Правда, оказалось, там были чуть другие 6 символов.
      +7
      https://www.destroyallsoftware.com/talks/wat ;-)
        0
        а что делает код по ссылке? напишите под спойлером хотя бы =)
          0
          Выводит алерт, там просто
          alert("wtf")
          
            0
            выводит alert(«wtf»)
            0
            Некоторые люди поднимают тему обфускации, но, честно говоря, для этого есть методы получше.

            С этого момента можно подробнее?
              0
              base64, использование замен символов на их \x и \u коды всё ещё неплохо обфусцирует.
                +1
                Нет в js нормальной обфускации и быть не может по определению. Вот здесь: https://jsplumbtoolkit.com/ ребята защищают свой продукт (не дешевый ни разу) похожими техниками (как в статье). Толку? У меня один вечер ушел на то чтобы отвязать их продукт от домена (не корысти ради, реально интересно было).
                  0
                  Отвязка от домена это в любом случае нахождение куска кода, который проверяет этот самый домен с зашифрованными внутри себя разрешенными доменами и удаление этого куска. Зная, что такой код должен выполняться как можно раньше, то найти такой кусок довольно просто.

                  Основная цель обфускации — максимально усложнить понимание логики кода. С этой задачей обфускаторы справляются отлично. Речь, разумеется, о объеме кода 500+ строк.

                  Например, что делает этот код?
                  https://gist.github.com/sanex3339/556d5e66ec787f16a6a0fb4b5fd729f6
                    +2
                    Вы верно шутите? И судя по всему код по моей ссылке не смотрели. Что касается Вашего упражнения — каждый вызов функции заменяем на toString() и смотрим исходник «обфусцированных» функций.
                      0
                      В результате .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();
                          }
                      
                        +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
                        И так далее по списку.
                        если текста РЕАЛЬНО много — можно автоматизировать процесс, парсеров хватает.
                          0
                          http://www.jsnice.org помогает, хотя в логику особо и не всматривался. Я так понимаю, там что-то связанное с (де)кодированием в base64 и почему-то метод с названием html5 валяется.
                  0
                  одна из тех вещей которые прикольные в теории, но если вы используете их в реальной жизни — с вами серьезно что-то не так…
                    +1
                    Обсфукация в js и php — рядовые вещи.
                    0
                    А говорили С++ сложный и запутанный… Как же они ошибались…
                      0
                      +(«1» + «1») === 11

                      Вот тут я не очень понял, почему не 12?
                      Ну то есть я вижу, что в девелопере все верно, но можно пояснить, ведь интуитивно — инкрементация должна была произойти до сравнения?
                        0
                        Хм. вроде сам понял. Инкрементация это если ++

                        А тут «ничего» добавляется к «11» и получается 11.
                        • UFO just landed and posted this here
                            +1
                            (ветку комментариев не читай, свой ответ пиши)

                            del
                            +1
                            Этот сабсет, ИМХО, гораздо полезнее брейнфака. На правах петросяна. Если жизнь и самозародится когда-то в недрах интернета, то как-то вот так вот.
                              +1
                              Бывает очень весело =)

                              {}[0] == 0 && 0 != {}[0] // true
                              

                              • UFO just landed and posted this here
                                  +4
                                  А оно вообще ошибки синтаксиса умеет выдавать? :)
                                  • UFO just landed and posted this here
                                    +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
                                  0
                                  +[] === +![]

                                  По-прежнему забавно. Для ToNumber "[]" проходит через ToPrimitive, который пуст. А для ToBoolean "[]" это объект, который всегда true.
                                    0
                                    Какое-то дежавю, это ведь все уже было на хабре, недавно еще и статья про цикад и цсс начала снова появляться в ленте.

                                    Only users with full accounts can post comments. Log in, please.