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


Извините.


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


Источники:


Поделиться с друзьями
-->

Комментарии (28)


  1. ripatti
    10.10.2016 10:16
    +2

    Сразу же вспомнил про эту задачу https://ipsc.ksp.sk/2015/real/problems/m.html.
    Правда, оказалось, там были чуть другие 6 символов.


  1. EndUser
    10.10.2016 10:52
    +7

    https://www.destroyallsoftware.com/talks/wat ;-)


  1. m0sk1t
    10.10.2016 11:02

    а что делает код по ссылке? напишите под спойлером хотя бы =)


    1. freetonik
      10.10.2016 11:15

      Выводит алерт, там просто

      alert("wtf")
      


    1. Gazaret
      10.10.2016 15:03

      выводит alert(«wtf»)


  1. trik
    10.10.2016 11:15

    Некоторые люди поднимают тему обфускации, но, честно говоря, для этого есть методы получше.

    С этого момента можно подробнее?


    1. LoadRunner
      10.10.2016 11:19

      base64, использование замен символов на их \x и \u коды всё ещё неплохо обфусцирует.


    1. gearbox
      10.10.2016 11:26
      +1

      Нет в js нормальной обфускации и быть не может по определению. Вот здесь: https://jsplumbtoolkit.com/ ребята защищают свой продукт (не дешевый ни разу) похожими техниками (как в статье). Толку? У меня один вечер ушел на то чтобы отвязать их продукт от домена (не корысти ради, реально интересно было).


      1. sanex3339
        10.10.2016 13:26

        Отвязка от домена это в любом случае нахождение куска кода, который проверяет этот самый домен с зашифрованными внутри себя разрешенными доменами и удаление этого куска. Зная, что такой код должен выполняться как можно раньше, то найти такой кусок довольно просто.

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

        Например, что делает этот код?
        https://gist.github.com/sanex3339/556d5e66ec787f16a6a0fb4b5fd729f6


        1. gearbox
          10.10.2016 15:02
          +2

          Вы верно шутите? И судя по всему код по моей ссылке не смотрели. Что касается Вашего упражнения — каждый вызов функции заменяем на toString() и смотрим исходник «обфусцированных» функций.


          1. 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();
                }
            


            1. 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
              И так далее по списку.
              если текста РЕАЛЬНО много — можно автоматизировать процесс, парсеров хватает.


            1. Hauts
              10.10.2016 21:08

              http://www.jsnice.org помогает, хотя в логику особо и не всматривался. Я так понимаю, там что-то связанное с (де)кодированием в base64 и почему-то метод с названием html5 валяется.


  1. romanonthego
    10.10.2016 12:05

    одна из тех вещей которые прикольные в теории, но если вы используете их в реальной жизни — с вами серьезно что-то не так…


    1. saboteur_kiev
      10.10.2016 14:37
      +1

      Обсфукация в js и php — рядовые вещи.


  1. sborisov
    10.10.2016 13:51

    А говорили С++ сложный и запутанный… Как же они ошибались…


  1. saboteur_kiev
    10.10.2016 14:27

    +(«1» + «1») === 11

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


    1. saboteur_kiev
      10.10.2016 14:31

      Хм. вроде сам понял. Инкрементация это если ++

      А тут «ничего» добавляется к «11» и получается 11.


    1. Aingis
      10.10.2016 14:34
      +1

      Потому что складываются строки. Если хотя бы один из операндов строка, делается приведение к строке. Унарный плюс просто приводит к числу, короткий аналог Number().

      "1" + "1" // "11"
      +("11") // 11
      


    1. nomn
      11.10.2016 10:15
      +1

      (ветку комментариев не читай, свой ответ пиши)

      del


  1. trapwalker
    10.10.2016 15:02
    +1

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


  1. Grammka
    10.10.2016 15:26
    +1

    Бывает очень весело =)

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


    1. Aingis
      10.10.2016 16:05
      +1

      Внезапно

      ({}[0] == 0 && 0 != {}[0]) // false
      ({}[0] == 0) // false
      


      1. NINeOneone
        10.10.2016 17:13
        +4

        А оно вообще ошибки синтаксиса умеет выдавать? :)


        1. Aingis
          10.10.2016 19:56

          Всё корректно.

          @ // Uncaught SyntaxError: Invalid or unexpected token
          


      1. 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


  1. lasthand
    11.10.2016 10:29

    +[] === +![]

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


  1. Large
    12.10.2016 12:52

    Какое-то дежавю, это ведь все уже было на хабре, недавно еще и статья про цикад и цсс начала снова появляться в ленте.