» Перевод статьи Detecting PhantomJS Based Visitors | Неплохое обсуждение статьи на Hacker News

Статья старая, помидорами не кидайтесь — лучше делитесь опытом в комментариях.

В наши дни во многих инцидентах по безопасности используется автоматизация (со стороны злоумышленников). Web-scraping, повторное использование паролей, click-fraud — все это совершается злоумышленниками в попытках (зачастую успешных) замаскироваться под обычного пользователя, то есть по сути выглядеть для сервера как броузер обычного пользователя. Как владелец сайта, вы наверно хотите быть уверены в том что обслуживаете людей а не бездушные железки, а как поставщик сервиса вы наверно хотите еще и доступ дать к своему контенту через api, а не через тяжелый и глючный web-интерфейс.

Предположим что у вас уже есть простенькая проверка для cUrl и ему подобных посетителей, и она достаточно эффективна. Следующим шагом ожидаемо будет поставить проверку на то что ваши клиенты настоящие и пользуются настоящим броузером, с тупым и глючным UI, а не боты на поделках типа PhantomJS или SlimerJS.

В этой статье мы рассмотрим пару приемов для определения фантомных ботов. Я рассматриваю только фантом, так как он более популярен, но многие моменты могут быть использованы и для SlimerJS и ему подобных.

Важно! Рассматриваемые методы применимы к обоим веткам фантома (1.x и 2.x), если явно не оговорено иное.

Для начала: можно ли определить фантома даже не отвечая ему (то есть исключительно по его http запросу)?

HTTP-стек


Вы, должно быть, знаете что фантом построен на QT фрейворке. Так вот, Qt реализует HTTP стек несколько иначе, чем другие современные броузеры.

Для начала давайте взглянем на простенький http запрос Хрома:

GET / HTTP/1.1
Host: localhost:1337
Connection: keep-alive
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36
Accept-Encoding: gzip, deflate, sdch
Accept-Language: en-US,en;q=0.8,ru;q=0.6

А теперь этот же запрос в фантоме:

GET / HTTP/1.1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X) AppleWebKit/534.34 (KHTML, like Gecko) PhantomJS/1.9.8 Safari/534.34
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Connection: Keep-Alive
Accept-Encoding: gzip
Accept-Language: en-US,*
Host: localhost:1337

Обратите внимание, хедеры фантома отличаются от хрома (как и от большинства современных броузеров):

  • Host хедер идет последним (у хрома первым)
  • значение хидера Connection (обратите внимание на регистр)
  • Accept-Encoding у фантома только gzip
  • User-Agent содержит “PhantomJS”

Проверка на различие этих хедеров на стороне сервера может помочь определить фантомный заход.

Но насколько безопасно доверять такой проверке? Если злоумышленник использует прокси для перезаписи этих хедеров то в общем то ему не составит труда мимикрировать под нормальный броузер.

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

Проверка User-Agent на клиенте


мы можем не верить полученному в запросе User-Agent, но что насчет значения в клиенте?

if (/PhantomJS/.test(window.navigator.userAgent)) {
    console.log("PhantomJS environment detected.");
}

К сожалению это так же легко поменять как хедер в запросе, так что этого явно не достаточно.

Плагины


navigator.plugins содержит массив плагинов установленных в броузере. Обычно он содержит что то вроде Flash, ActiveX, поддержку для java апплетов или Default Browser Helper который указывает на то что этот броузер — дефолтный в OS X. Наши исследования показывают что большинство установок «с нуля» общераспространенных броузеров содержат хотя бы один дефолтный плагин — даже на мобилах.

Этим и отличается PhantomJs — он не ставит никаких плагинов и более того — не дает никаких возможностей их установить ( PhantomJS API).

Следующая проверка может быть вполне полезной:

if (!(navigator.plugins instanceof PluginArray) || navigator.plugins.length == 0) {
    console.log("PhantomJS environment detected.");
} else {
    console.log("PhantomJS environment not detected.");
}

С другой стороны — крайне просто подменить массив navigator.plugins исполняя js код ДО подгрузки страницы (как здесь).

Также никакого труда не составляет создать кастомную сборку с настоящими установленными плагинами. Это гораздо легче чем кажется потому что QT, на котором построен фантом, предоставляет возможности для подключения npapi плагинов.

Timing


Другой интересный момент — это то как PhantomJS рубит JavaScript диалоги:

var start = Date.now();
alert('Press OK');
var elapse = Date.now() - start;
if (elapse < 15) {
    console.log("PhantomJS environment detected. #1");
} else {
    console.log("PhantomJS environment not detected.");
}

После нескольких проверок можно предположить что если диалог закрывается меньше чем за 15 милисекунд, то скорее всего броузер не контролируется человеком. Но использование этой техники предполагает некоторый негатив со стороны реальных пользователей, которые вынуждены будут закрывать непонятные окошки. (на самом деле этот момент можно обойти, привязавшись к каким либо действиям пользователя, например предлагая что либо при наведении на какой либо элемент — тот момент когда пользователь говорит «нет, спасибо». Тоже немного навязчиво, но по крайней мере хоть какой то смысл в происходящем с точки зрения пользователя — прим. перевод.)

Глобалы


PhantomJS 1.x предоставляет два вида глобалов:

if (window.callPhantom || window._phantom) {
  console.log("PhantomJS environment detected.");
} else {
  console.log("PhantomJS environment not detected.");
}

Но это часть экспериментальной технологии, так что все еще может поменяться.

Фишки JavaScript движка


PhantomJS 1.x и 2.x используют не самые свежие версии WebKit, что подразумевает отсутствие новых модных плюшек, внедренных уже в последние версии броузеров. Это автоматически распространяется и на JS движок, то есть некоторые свойства и методы ведут себя иначе или вообще отсутствуют в PhantomJS (тут правда непонятно чем это все отличается от просто старого броузера — прим. перев.)

Один из таких методов — Function.prototype.bind, отсутствующий в PhantomJS 1.x и старше. Следующий пример проверяет — есть ли bind у прототипа функции и если есть — то точно ли он нативный а не зашимленный.

(function () {
  if (!Function.prototype.bind) {
    console.log("PhantomJS environment detected. #1");
    return;
  }
  if (Function.prototype.bind.toString().replace(/bind/g, 'Error') != Error.toString()) {
    console.log("PhantomJS environment detected. #2");
    return;
  }
  if (Function.prototype.toString.toString().replace(/toString/g, 'Error') != Error.toString()) {
    console.log("PhantomJS environment detected. #3");
    return;
  }
  console.log("PhantomJS environment not detected.");
})();
</script>

Если вам этот код кажется слегка непонятным, можно взглянуть на небольшое объяснение в деталях здесь (видео).

Stack Traces


Ошибки которые генерит JavaScript код обработанные PhantomJS через команду evaluate содержат уникальный стек по которому можно определить «безголовый» броузер.

Предположим что PhantomJS вызывает обработку в следующем коде:

var err;
try {
  null[0]();
} catch (e) {
  err = e;
}
if (indexOfString(err.stack, 'phantomjs') > -1) {
  console.log("PhantomJS environment detected.");
} else {
  console.log("PhantomJS environment is not detected.");
}

Обратите внимание — здесь у нас кастомная indexOfString() функция, (реализацию мы оставили за скобками предполагая что у читателя не вызовет никаких затруднений реализовать ее) так как нативная String.prototype.indexOf может быть подменена PhantomJS (пользовательским скриптом) и возвращать отрицательный результат. (что в общем то тоже нетрудно проверить — прим. перевод.).

Так, а как теперь PhantomJS заставить исполнить этот код? Одна из техник — переписать наиболее часто используемые DOM функции которые с большой вероятностью будут вызваны. Например код ниже переписывает document.querySelectorAll что бы перехватить stack trace броузера:

var html = document.querySelectorAll('html');
var oldQSA = document.querySelectorAll;
Document.prototype.querySelectorAll = Element.prototype.querySelectorAll = function () {
  var err;
  try {
    null[0]();
  } catch (e) {
    err = e;
  }
  if (indexOfString(err.stack, 'phantomjs') > -1) {
    return html;
  } else {
    return oldQSA.apply(this, arguments);
  }
};

Итого


В этой статье мы рассмотрели 7 разных техник определения PhantomJS как на сервере так и на клиенте. Комбинируя результаты проверки с обратной связью (например замеряя скорость рендеринга или ломая сессионную куку) можно в принципе устроить сложностей PhantomJS посетителям. Но держите в голове что эти техники не являются строгими и безошибочными (по факту на кастомных сборках ни одна не работает — прим. перевод.) и продвинутый противник может прорвать оборону. Для углубления в тему мы рекомендуем к просмотру нашу презентацию (видео, слайды). Есть также GitHub репа с примерами и возможными путями обхода проверок.

Спасибо за внимание и удачной охоты!
Поделиться с друзьями
-->

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


  1. KlimovDm
    24.09.2016 15:43
    +2

    >>>и продвинутый противник может прорвать оборону

    Где оборона то?
    7 пунктов.
    Один — на серверной стороне (в этом есть какой-то смысл). Анализ заголовков. Смешно, даже останавливаться не буду.
    ШЕСТЬ на клиентской стороне. Елки-палки, ничего более бессмысленного не видел. Пришел запрос, сервер на него ответил и отдал все, что от него просили. Всё, данные получены, в полном обьеме — теперь их «хозяин» тот, кто их запросил и получил — что хочет с ними, то и делает.


    1. gonzazoid
      24.09.2016 16:08

      >Один — на серверной стороне (в этом есть какой-то смысл). Анализ заголовков. Смешно, даже останавливаться не буду.
      А что еще Вы предлагаете анализировать на сервере? (не думаю что на tcp уровне он будет отличаться от обычного броузера)

      >Пришел запрос, сервер на него ответил и отдал все, что от него просили. Всё, данные получены, в полном обьеме — теперь их «хозяин» тот, кто их запросил и получил — что хочет с ними, то и делает.

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

      А вообще конечно идем на https://github.com/ariya/phantomjs/issues и выбираем подходящие для детекта.


      1. KlimovDm
        24.09.2016 16:30
        +1

        Я вам написал только о том, что в этом переводе есть слово «оборона», а по факту описано только несколько ненадёжных методик определения некоторого бота.


      1. starius
        24.09.2016 17:01

        не думаю что на tcp уровне он будет отличаться от обычного броузера

        Из TCP можно узнать операционную систему и сверять её с user agent.


      1. darth_dolphi
        24.09.2016 18:41

        Не понимаю как это может мне помешать открыть следующий линк и так дальше. Все что происходит на клиентской стороне можно отловить и хакнуть.
        P/s если не хотите что бы вас нещадно скрапили, сделайте фид и забудьте о подобных проблемах. Ибо если его нет вас все равно будут скрапить.


      1. Xalium
        24.09.2016 18:41
        +2

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

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


  1. gonzazoid
    24.09.2016 16:02

    промахнулся


  1. Fen1kz
    24.09.2016 16:31
    +7

    А смысл? Никак не можете примириться с тем, что отдавая данные клиенту вы отдаете их ему и уже он властен над ними? Не понятна ситуация, при которой вы потратите кучу сил и проверок на то, чтобы сохранить данные от хакера, когда чувак с расширением на хроме или 10 китайцев так же распарсят ваш сайт.
    Или это будет целая серия :D
    "Определяем китайцев", "определяем юзер-скрипты", "определяем что пользователь не запомнил данные после посещения сайта", "вставляем ватермарки в body"?


  1. TFStudio
    24.09.2016 16:44
    +3

    selenium вы не определите так просто.


  1. softaria
    24.09.2016 16:48

    А есть ли смысл бороться именно с phantom? Ведь ботов можно написать на чем угодно.


    1. KlimovDm
      24.09.2016 17:04

      Я бы расширил вопрос — а есть ли смысл бороться с ботами? Как справедливо заметил Fen1kz 10 китайцев заменяют одного бота :) Априоре понятно — показал контент наружу — все, отдал. Хочешь спрятать — закрывай.

      P.S. Я встречал всего одно бота, с которым пришлось реально бороться — злобный WebIndex. И то причина была не том, что он парсил сайт. Просто его написали программеры низкого уровня. Он какое-то время собирает ссылки на сайт, а потом в определенное время пытается их сразу все забрать и проиндексировать. Именно сразу, никаких таймаутов между запросами. Бомбилка еще та была.


      1. softaria
        24.09.2016 19:43
        +1

        Иногда, есть. Пример — браузерные игры, где прокачка персонажей ботами убивает у других игроков интерес к игре.


        1. KlimovDm
          24.09.2016 19:46

          Хм, да, может быть. Я просто очень сильно далек от этой области, даже на ум не пришло.


          1. softaria
            24.09.2016 21:00

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


            1. Sabbaot
              25.09.2016 15:28

              можно, пожалуйста, подробней?


              1. softaria
                25.09.2016 16:37

                С яваскриптовыми ботами можно как-то бороться. А вот таких отличить от человека вообще не получается. Там при записи сценария ты показываешь прямоугольную область экрана и координаты клика отсчитываются от неё. Когда скрипт запускается он повторяет такой клик (ему еще можно сказать слегка рандомизировать клики — прибавлять маленькое случайное число к обоим координатам). Если скрипт не находит на экране заданную область, он просто перестает работать и сообщает о проблеме.
                Ловить таких очень дорого и сложно.


                1. Saffron
                  25.09.2016 19:19

                  Я в своё время пользовался sikuli для автоматизации игрового процесса. А что рекомендуют использовать в современных реалиях?


                  1. softaria
                    25.09.2016 19:37

                    Вот не знаю. Я как раз занимался борьбой с такими ботами :)


                  1. Areso
                    26.09.2016 11:05

                    У меня самоделка на ардуине+серва, жмет клавиши для автоматизации игрового процесса. Работает под любой ОС, и даже с теми играми где защита от мышиных кликеров (в т.ч. игровых мышей и клавиатур) и ввод от winapi игнорируется и принимается только, как я предполагаю, через DirectInput.
                    Благодаря механике, и таймеру с рандомным смещением выглядит 100% натурально. За исключением, разве что, что жмякает по клавиатуре сутками напролет.
                    Еще не забанили :)


                1. nikolajpor
                  26.09.2016 11:16

                  внедрение бота в код игры(ingame, autoit), или вовсе без игры (OOG — out of game, c++,c#,delphi), но все эти боты ломаются при обновлениях игры или ее упаковке, поэтому интересуют конкретные инструменты, как вы уже выше сказали «ява+скрипты», при которых можно научить бота распознавать определенные формы. и да:

                  Ловить таких очень дорого и сложно.


      1. miolini
        25.09.2016 01:43

        Страниц может быть 10 млн. Тогда можно остановить краулинг уже на первом десятке.


        1. KlimovDm
          25.09.2016 07:53
          +1

          Их здесь 10 млн.!, в ацеполе все… хм, извините, отвлекся :) Китайцев все равно больше.

          Вопрос был — ЗАЧЕМ? softaria привел хороший (неожиданный для меня) пример.
          А вы на какой вопрос отвечайте?


  1. Saffron
    24.09.2016 18:37

    Меня удивляет, почему автор поста считает, что в браузере должен быть включён джаваскрипт. Стандарты HTML этого не требуют, и я как пользователь с ними солидарен. Отключаю везде, где только можно. Если запрещают смотреть без них, выкачиваю страницу тем самым phantom.js


    1. Smi1e
      24.09.2016 18:57
      +4

      А вы терпеливый.


      1. Saffron
        24.09.2016 19:58

        Слава богам, гугл ещё не разучился работать без javascript, и опцию saved copy всё ещё предоставляет.


  1. xpoft
    24.09.2016 18:42

    Помимо, действительно глючного PhantomJS, есть ещё CEF. Chromium Embedded Framework. Вот где благодать.


  1. thauquoo
    24.09.2016 18:42
    +1

    Предпочитаю писать ботов на QtWebkit и C++. Использование полноценного браузерного движка позволяет легко обходить разнообразные защиты от автоматизированного скачивания и обработки данных, а возможность имитировать выделение и копирование, как будто бы это делал пользователь, сводит на нет усилия разработчиков сайта добавить кучу CSS и JS мусора, который потом вычисляется и становится невидимым.

    Интересно, как можно задетектить такого бота?


    1. Antelle
      24.09.2016 20:46

      Можно отследить консистентность движения мышки или клавиатуры. Но и этому бота научить проще, чем сделать проверку.


  1. VJean
    24.09.2016 18:42
    +3

    P.S. Я встречал всего одно бота, с которым пришлось реально бороться — злобный WebIndex.
    Именно сразу, никаких таймаутов между запросами. Бомбилка еще та была.

    а что сложного? либо iptables:
    -A INPUT -i venet0:0 -p tcp -m tcp -m multiport --dports 80,443 -m state --state NEW -m recent --update --seconds 60 --hitcount 10 -m comment --comment "nginx drop hits" -j DROP
    -A INPUT -p tcp -m tcp -m multiport --dports 80,443 -m state --state NEW -m comment --comment "nginx" -j ACCEPT
    


    либо использовать модуль nginx ngx_http_limit_conn_module


    1. KlimovDm
      24.09.2016 19:38
      +1

      Я разве где-нибудь написал о сложности или проблеме? К чему это вы? Или просто невнимательно прочитали?

      Если говорить о iptables, то мне больше нравится вариант с hashlimit, а не resent (и тесты показали, что это действительно так). Да и погибче будет. Однако, если клиент включит keep-alive — ваш вариант не сработает.


  1. igor_suhorukov
    24.09.2016 20:55

    Да отлично работает с такими сайтами webdriver api и firefox/chrome


  1. numitus2
    25.09.2016 15:28
    +1

    Обойти все это очень просто. Достаточно нескольких изменений в исходном коде.


    1. gonzazoid
      25.09.2016 15:31

      Да, в переводе есть примечание о том что кастомная сборка фантома обойдет все известные сборщику методы детекта.


  1. kirill3333
    26.09.2016 11:32
    +1

    Не нашел в тексте — набор слайдов по данной тематике Detecting headless browsers