Определяем Phantom-ных ботов

» Перевод статьи 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 репа с примерами и возможными путями обхода проверок.

Спасибо за внимание и удачной охоты!
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 35

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

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

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

      А вообще конечно идем на https://github.com/ariya/phantomjs/issues и выбираем подходящие для детекта.
      • UFO just landed and posted this here
          0
          не думаю что на tcp уровне он будет отличаться от обычного броузера

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

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

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

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

                +3
                selenium вы не определите так просто.
                  0
                  А есть ли смысл бороться именно с phantom? Ведь ботов можно написать на чем угодно.
                  • UFO just landed and posted this here
                      +1
                      Иногда, есть. Пример — браузерные игры, где прокачка персонажей ботами убивает у других игроков интерес к игре.
                      • UFO just landed and posted this here
                          0
                          Тут, кстати, самыми проблемными являются не js боты, а те, которые тупо кликают мышью по окну честно открытого браузера, ориентируясь по заранее записанным фрагментам картинки.
                            0
                            можно, пожалуйста, подробней?
                              0
                              С яваскриптовыми ботами можно как-то бороться. А вот таких отличить от человека вообще не получается. Там при записи сценария ты показываешь прямоугольную область экрана и координаты клика отсчитываются от неё. Когда скрипт запускается он повторяет такой клик (ему еще можно сказать слегка рандомизировать клики — прибавлять маленькое случайное число к обоим координатам). Если скрипт не находит на экране заданную область, он просто перестает работать и сообщает о проблеме.
                              Ловить таких очень дорого и сложно.
                                0
                                Я в своё время пользовался sikuli для автоматизации игрового процесса. А что рекомендуют использовать в современных реалиях?
                                  0
                                  Вот не знаю. Я как раз занимался борьбой с такими ботами :)
                                    0
                                    У меня самоделка на ардуине+серва, жмет клавиши для автоматизации игрового процесса. Работает под любой ОС, и даже с теми играми где защита от мышиных кликеров (в т.ч. игровых мышей и клавиатур) и ввод от winapi игнорируется и принимается только, как я предполагаю, через DirectInput.
                                    Благодаря механике, и таймеру с рандомным смещением выглядит 100% натурально. За исключением, разве что, что жмякает по клавиатуре сутками напролет.
                                    Еще не забанили :)
                                    0
                                    внедрение бота в код игры(ingame, autoit), или вовсе без игры (OOG — out of game, c++,c#,delphi), но все эти боты ломаются при обновлениях игры или ее упаковке, поэтому интересуют конкретные инструменты, как вы уже выше сказали «ява+скрипты», при которых можно научить бота распознавать определенные формы. и да:
                                    Ловить таких очень дорого и сложно.
                            0
                            Страниц может быть 10 млн. Тогда можно остановить краулинг уже на первом десятке.
                            • UFO just landed and posted this here
                              0

                              Одной из целей определения бота может быть известный в узких кругах клоакинг. Когда боту поисковой системы не показывается агрессивная реклама, за которую поисковики понижают в поиске. У поисковых систем есть специальные боты для клоакинг-детекта, которые максимально маскируются под человека, поэтому проверка по юзер-агенту здесь не поможет. А вот комплекс специфических технических уловок — вполне.

                            0
                            Меня удивляет, почему автор поста считает, что в браузере должен быть включён джаваскрипт. Стандарты HTML этого не требуют, и я как пользователь с ними солидарен. Отключаю везде, где только можно. Если запрещают смотреть без них, выкачиваю страницу тем самым phantom.js
                              +4
                              А вы терпеливый.
                                0
                                Слава богам, гугл ещё не разучился работать без javascript, и опцию saved copy всё ещё предоставляет.
                              0
                              Помимо, действительно глючного PhantomJS, есть ещё CEF. Chromium Embedded Framework. Вот где благодать.
                              • UFO just landed and posted this here
                                  0

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

                                  +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
                                  • UFO just landed and posted this here
                                    0
                                    Да отлично работает с такими сайтами webdriver api и firefox/chrome
                                      +1
                                      Обойти все это очень просто. Достаточно нескольких изменений в исходном коде.
                                        0
                                        Да, в переводе есть примечание о том что кастомная сборка фантома обойдет все известные сборщику методы детекта.
                                        +1
                                        Не нашел в тексте — набор слайдов по данной тематике Detecting headless browsers

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