Статья старая, помидорами не кидайтесь — лучше делитесь опытом в комментариях.
В наши дни во многих инцидентах по безопасности используется автоматизация (со стороны злоумышленников). 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)
Fen1kz
24.09.2016 16:31+7А смысл? Никак не можете примириться с тем, что отдавая данные клиенту вы отдаете их ему и уже он властен над ними? Не понятна ситуация, при которой вы потратите кучу сил и проверок на то, чтобы сохранить данные от хакера, когда чувак с расширением на хроме или 10 китайцев так же распарсят ваш сайт.
Или это будет целая серия :D
"Определяем китайцев", "определяем юзер-скрипты", "определяем что пользователь не запомнил данные после посещения сайта", "вставляем ватермарки в body"?
softaria
24.09.2016 16:48А есть ли смысл бороться именно с phantom? Ведь ботов можно написать на чем угодно.
KlimovDm
24.09.2016 17:04Я бы расширил вопрос — а есть ли смысл бороться с ботами? Как справедливо заметил Fen1kz 10 китайцев заменяют одного бота :) Априоре понятно — показал контент наружу — все, отдал. Хочешь спрятать — закрывай.
P.S. Я встречал всего одно бота, с которым пришлось реально бороться — злобный WebIndex. И то причина была не том, что он парсил сайт. Просто его написали программеры низкого уровня. Он какое-то время собирает ссылки на сайт, а потом в определенное время пытается их сразу все забрать и проиндексировать. Именно сразу, никаких таймаутов между запросами. Бомбилка еще та была.softaria
24.09.2016 19:43+1Иногда, есть. Пример — браузерные игры, где прокачка персонажей ботами убивает у других игроков интерес к игре.
KlimovDm
24.09.2016 19:46Хм, да, может быть. Я просто очень сильно далек от этой области, даже на ум не пришло.
softaria
24.09.2016 21:00Тут, кстати, самыми проблемными являются не js боты, а те, которые тупо кликают мышью по окну честно открытого браузера, ориентируясь по заранее записанным фрагментам картинки.
Sabbaot
25.09.2016 15:28можно, пожалуйста, подробней?
softaria
25.09.2016 16:37С яваскриптовыми ботами можно как-то бороться. А вот таких отличить от человека вообще не получается. Там при записи сценария ты показываешь прямоугольную область экрана и координаты клика отсчитываются от неё. Когда скрипт запускается он повторяет такой клик (ему еще можно сказать слегка рандомизировать клики — прибавлять маленькое случайное число к обоим координатам). Если скрипт не находит на экране заданную область, он просто перестает работать и сообщает о проблеме.
Ловить таких очень дорого и сложно.Saffron
25.09.2016 19:19Я в своё время пользовался sikuli для автоматизации игрового процесса. А что рекомендуют использовать в современных реалиях?
Areso
26.09.2016 11:05У меня самоделка на ардуине+серва, жмет клавиши для автоматизации игрового процесса. Работает под любой ОС, и даже с теми играми где защита от мышиных кликеров (в т.ч. игровых мышей и клавиатур) и ввод от winapi игнорируется и принимается только, как я предполагаю, через DirectInput.
Благодаря механике, и таймеру с рандомным смещением выглядит 100% натурально. За исключением, разве что, что жмякает по клавиатуре сутками напролет.
Еще не забанили :)
nikolajpor
26.09.2016 11:16внедрение бота в код игры(ingame, autoit), или вовсе без игры (OOG — out of game, c++,c#,delphi), но все эти боты ломаются при обновлениях игры или ее упаковке, поэтому интересуют конкретные инструменты, как вы уже выше сказали «ява+скрипты», при которых можно научить бота распознавать определенные формы. и да:
Ловить таких очень дорого и сложно.
miolini
25.09.2016 01:43Страниц может быть 10 млн. Тогда можно остановить краулинг уже на первом десятке.
Saffron
24.09.2016 18:37Меня удивляет, почему автор поста считает, что в браузере должен быть включён джаваскрипт. Стандарты HTML этого не требуют, и я как пользователь с ними солидарен. Отключаю везде, где только можно. Если запрещают смотреть без них, выкачиваю страницу тем самым phantom.js
xpoft
24.09.2016 18:42Помимо, действительно глючного PhantomJS, есть ещё CEF. Chromium Embedded Framework. Вот где благодать.
thauquoo
24.09.2016 18:42+1Предпочитаю писать ботов на QtWebkit и C++. Использование полноценного браузерного движка позволяет легко обходить разнообразные защиты от автоматизированного скачивания и обработки данных, а возможность имитировать выделение и копирование, как будто бы это делал пользователь, сводит на нет усилия разработчиков сайта добавить кучу CSS и JS мусора, который потом вычисляется и становится невидимым.
Интересно, как можно задетектить такого бота?Antelle
24.09.2016 20:46Можно отследить консистентность движения мышки или клавиатуры. Но и этому бота научить проще, чем сделать проверку.
VJean
24.09.2016 18:42+3P.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_moduleKlimovDm
24.09.2016 19:38+1Я разве где-нибудь написал о сложности или проблеме? К чему это вы? Или просто невнимательно прочитали?
Если говорить о iptables, то мне больше нравится вариант с hashlimit, а не resent (и тесты показали, что это действительно так). Да и погибче будет. Однако, если клиент включит keep-alive — ваш вариант не сработает.
kirill3333
26.09.2016 11:32+1Не нашел в тексте — набор слайдов по данной тематике Detecting headless browsers
KlimovDm
>>>и продвинутый противник может прорвать оборону
Где оборона то?
7 пунктов.
Один — на серверной стороне (в этом есть какой-то смысл). Анализ заголовков. Смешно, даже останавливаться не буду.
ШЕСТЬ на клиентской стороне. Елки-палки, ничего более бессмысленного не видел. Пришел запрос, сервер на него ответил и отдал все, что от него просили. Всё, данные получены, в полном обьеме — теперь их «хозяин» тот, кто их запросил и получил — что хочет с ними, то и делает.
gonzazoid
>Один — на серверной стороне (в этом есть какой-то смысл). Анализ заголовков. Смешно, даже останавливаться не буду.
А что еще Вы предлагаете анализировать на сервере? (не думаю что на tcp уровне он будет отличаться от обычного броузера)
>Пришел запрос, сервер на него ответил и отдал все, что от него просили. Всё, данные получены, в полном обьеме — теперь их «хозяин» тот, кто их запросил и получил — что хочет с ними, то и делает.
ну так грабят не одну страничку с сайта а раздел или весь сайт. Определив парсер можно временно забанить ip, что уже делает стоимость парсинга выше. Это ведь типичная ситуация меча и щита, причем щит публичный и любой его может рассмотреть. В этой ситуации не может быть идеальных решений.
А вообще конечно идем на https://github.com/ariya/phantomjs/issues и выбираем подходящие для детекта.
KlimovDm
Я вам написал только о том, что в этом переводе есть слово «оборона», а по факту описано только несколько ненадёжных методик определения некоторого бота.
starius
Из TCP можно узнать операционную систему и сверять её с user agent.
darth_dolphi
Не понимаю как это может мне помешать открыть следующий линк и так дальше. Все что происходит на клиентской стороне можно отловить и хакнуть.
P/s если не хотите что бы вас нещадно скрапили, сделайте фид и забудьте о подобных проблемах. Ибо если его нет вас все равно будут скрапить.
Xalium
Грабить могут и в личных целях, чтобы уже потом оптом на одной странице проанализировать товар и выбрать нужное, а не открывать кучу страниц в браузере и прыгать между этими страницами для стравнения нужного товара.
Сам так часто делаю.