无头浏览器

前言:

无头浏览器(Headless browser)指没有用户图形界面的(GUI)的浏览器,目前广泛运用于web爬虫和自动化测试中。随着反爬虫和反反爬虫对抗技术的升级,越来越多的爬虫开始使用无头浏览器伪装成正常用户绕过反爬虫策略。

我们如何区分这些无头浏览器和正常浏览器?从Server Side分析用户行为进行检测是一劳永逸的方法,但成本和难度都很大。

不过通过无头浏览器的一些特性。我们也可以从从Client Side找出一些不同来。下面以醉受欢迎的PhantomJS(2.x版本)为例,介绍一些识别的方法,对于其他的无头浏览器,如Slimer JS这些方法也可以参考

HTTP Header

  • PhantomJS

    GET / HTTP/1.1
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
    User-Agent: Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/538.1 (KHTML, like Gecko) PhantomJS/2.1.1 Safari/538.1
    Connection: Keep-Alive
    Accept-Encoding: gzip, deflate
    Accept-Language: zh-CN,en,*
    Host: test.com
  • Chrome

    GET / HTTP/1.1
    Host: test.com
    Connection: keep-alive
    Upgrade-Insecure-Requests: 1
    User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
    DNT: 1
    Accept-Encoding: gzip, deflate, sdch
    Accept-Language: zh-CN,zh;q=0.8
  • IE 11

    GET / HTTP/1.1
    Accept: image/gif, image/jpeg, image/pjpeg, application/x-ms-application, application/xaml+xml, application/x-ms-xbap, */*
    Accept-Language: zh-CN
    User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 10.0; WOW64; Trident/7.0; .NET4.0C; .NET4.0E; .NET CLR 2.0.50727; .NET CLR 3.0.30729; .NET CLR 3.5.30729)
    Accept-Encoding: gzip, deflate
    Host: test.com
    Connection: Keep-Alive
  • Firefox

    GET / HTTP/1.1
    Host: test.com
    User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:52.0) Gecko/20100101 Firefox/52.0
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
    Accept-Language: en-US,en;q=0.5
    Accept-Encoding: gzip, deflate
    Connection: keep-alive
    Upgrade-Insecure-Requests: 1
  • 可以看出PhantomJS的请求头和其他浏览器还是有一些区别的

    • Host头在最后边
    • Connection头的值Keep-Alive是大小写混合
    • User-Agent头里包含PhantomJS关键字

检测方案

  • 通过User-Agent,可以简单的识别出一部分PhantomJS浏览器:

    if (/PhantomJS/.test(navigator.userAgent)) {
      console.log("PhantomJS environment detected.");
    } else {
      console.log("PhantomJS environment not detected.");
    }
    
    // 提示: 这里只需要重写一下就看不出来了,所以不靠谱
    page.settings.userAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.120 Safari/537.36';
  • 浏览器插件

    // 大部分浏览器甚至移动端的浏览器都会默认安装至少一个插件,比如 Chrome 浏览器就会安装 PDF Viewer,Shockwave Flash 等插件,这些插件用 navigator.plugins 可以看到。
    而 PhantomJS 没有实现任何插件功能,也没有提供方法去增加插件。所以我们可以通过检查浏览器的插件数量来猜测访问网站的是不是无头浏览器:
    
    if (!(navigator.plugins instanceof PluginArray) || navigator.plugins.length == 0) {
      console.log("PhantomJS environment detected.");
    } else {
      console.log("PhantomJS environment not detected.");
    }
    
    // 绕过检查的方法也很简单,在页面加载之前修改 navigator 对象就行了
    page.onInitialized = function () {
      page.evaluate(function () {
          var oldNavigator = navigator;
          var oldPlugins = oldNavigator.plugins;
          var plugins = {};
          plugins.length = 1;
          plugins.__proto__ = oldPlugins.__proto__;
          window.navigator = {plugins: plugins};
          window.navigator.__proto__ = oldNavigator.__proto__;
      });
    };
    • 另外也可以定制自己的 PhantomJS,其实并不复杂,因为 PhantomJS 是基于 Qt 框架的,而 Qt 已经提供了原生的 API 供实现插件。
  • 操作时

    般用无头浏览器的爬虫为了防止阻塞都会自动关闭页面上的对话框,但它关闭的速度肯定比正常人手动关闭快很多 ,所以我们可以通过对话框被关闭的用时来判断对面是否是机器人

    var start = Date.now();
    alert('Press OK');
    var elapse = Date.now() - start;
    if (elapse < 15) {
      console.log("PhantomJS environment detected.");
    } else {
      console.log("PhantomJS environment not detected.");
    }
    
    // 如果对话框在 15 毫秒内就被关闭,很有可能是无头浏览器在控制。
    // 但是这种方法有个弊端——会打扰正常用户,而且只需增加一些延时就可绕过
    page.onAlert = page.onConfirm = page.onPrompt = function () {
      for (var i = 0; i < 1e8; i++) {
      }
      return "a";
    };
  • 窗口特征

    if (window.outerWidth === 0 || window.outerHeight === 0){
    console.log("PhantomJS environment detected.");
    }
    • 不过这样在 Chrome 里会有一个 bug,当页面在隐藏选项卡中加载,例如从上一个会话恢复时,页面的 outerWidthouterHeight 也会为 0。
    • 另外可以检查键盘、鼠标、触摸屏交互,通过 onmouseoveronkeydown 等函数来判断对方是不是真实用户。
    • 但是 PhantomJS 也提供了 sendEvent 这个函数向页面发送事件
    • 还可以使用 jQuery 的 trigger() 主动触发事件:
    page.includeJs("http://ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.min.js", function(){
      // do anything
    });
    $('#some_element').trigger('hover');
  • 全局属性

    • PhantomJS 提供了两个全局属性:window.callPhantomwindow._phantom
    if (window.callPhantom || window._phantom) {
    console.log("PhantomJS environment detected.");
    } else {
    console.log("PhantomJS environment not detected.");
    }
    • 不过这两个是实验性功能,未来很可能会被替换。
    • 当然也可以通过重写绕过
    page.onInitialized = function () {
      page.evaluate(function () {
          var p = window.callPhantom;
          delete window._phantom;
          delete window.callPhantom;
          Object.defineProperty(window, "myCallPhantom", {
              get: function () {
                  return p;
              },
              set: function () {
              }, enumerable: false});
          setTimeout(function () {
              window.myCallPhantom();
          }, 1000);
      });
    };
    page.onCallback = function (obj) {
      console.log('profit!');
    };
    • 其他一些 JavaScript 引擎的独有全局属性
    window.Buffer //nodejs
    window.emit //couchjs
    window.spawn //rhino
    window.webdriver //selenium
    window.domAutomation (or window.domAutomationController) //chromium based automation driver
    
    
    
  • HTML5 和 JavaScript 新特性

    • PhantomJS 使用的 Webkit 引擎相对较旧,这意味着很多新浏览器才支持的特性可能在 PhantomJS 中缺失。
      • WebAudio
      • WebRTC
      • WebSocket
      • WebGL
      • FileAPI
      • CSS 3
      • Device APIs
      • BatteryManager
    • 另外,JavaScript 引擎的一些原生方法和属性在PhantomJS中不一样或者不存在
    (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.");
    })();
  • 堆栈追踪

    • 这是最有用的识别方法。因为开发者不可能花费大量的精力去修改Webkit的JavaScript引擎核心代码,使PhantomJS的堆栈跟踪写信和真是浏览器一样。
    • 当 PhantomJS 的 evaluate 方法报错时,它会产生独特的堆栈跟踪信息:
    var err;
    try {
    null[0]();
    } catch (e) {
    err = e;
    }
    if (err.stack.indexOf('phantomjs') > -1) {
    console.log("PhantomJS environment detected.");
    } else {
    console.log("PhantomJS environment is not detected.");
    
    
    // 那么如何让 PhantomJS 主动用 evaluate 执行这样的代码呢?我们可以设置蜜罐。
    // 比如重写常见的 DOM API 函数,当 PhantomJS 调用到了被我们重写的 DOM 函数时,
    // 它就会执行我们的代码
    
    Document.prototype.querySelectorAll = Element.prototype.querySelectorAll = function () {
    var err;
    try {
      null[0](); // Force throwing.
    } catch (e) {
      err = e;
    }
    if (err.stack.indexOf('phantomjs') > -1) {
      console.log("PhantomJS environment detected.");
    } else {
      console.log("PhantomJS environment is not detected.");
    }
    };
    
    // 上面的这段代码重写了 document.querySelectorAll 函数,如果爬虫调用 PhantomJS 时含有这样的代码
    page.onLoadFinished = function () {
      page.evaluate(function () {
        var divs = document.querySelectorAll('div');
        // do anything
      });
    };
    // 就会用 evaluate 执行被我们重写过的 querySelectorAll,从而被检测出来。

反击

  • 如果检测出对方使用了 PhantomJS,除了封他 IP,还有什么办法惩罚他呢? 由于 PhantomJS 2.x 已经支持 WebSocket,我们可以给他来个 WebSocket DDos 尝尝
(function () {
    for (var i = 0; i < 8000; i++) {
      new WebSocket('ws://victim.com/chat');
    }
  })();
// 另外,为了跨域请求的方便,很多 PhantomJS 爬虫会关闭 web-security 这个选项。
// 但关闭这个选项后,连本地的 file 域都可以访问了:
 var xhr = new XMLHttpRequest();
  xhr.open('GET', 'file:///etc/shadow', false);
  // xhr.open('GET', 'file:///C:/Windows/System32/drivers/etc/hosts', false);
  xhr.onload = function () {
    console.log(xhr.responseText);
  };
  xhr.onerror = function (e) {
    console.log('Error: ' + JSON.stringify(e));
  };
  xhr.send();
  • 如果权限够大,我们甚至可以读取运行 PhantomJS 的机器的密码。
  • 根据 PhantomJS 开发组的计划,WebRTC 将会被支持,到时候我们还能探测他的真实 IP和扫描他的内网。

总结

文中我们介绍了一些在 Client Side 检查 PhantomJS 的方法,对于一般的 PhantomJS 爬虫具有不错的识别效果。 当然,我们不能完全依赖于这些检测手段。对于有经验的爬虫作者,他们还是能通过 Hook JavaScript 代码绕过 Client Side 的检查。比如,重写 indexOf 函数,使 err.stack.indexOf('phantomjs') 恒为 -1,从而绕过我们对堆栈跟踪的检查。

在安全方面,当我们在生产环境使用无头浏览器时,应注意环境隔离,使用低权限运行,设置 --web-security=true

市面上常见的无头浏览器

软件名 介绍 支持语言
Awesomium 基于Chromium无图形界面浏览器引擎。 C++, .NET
benv Benv是node.js开发的无界面浏览器测试环境,用于测试客户端代码。 JavaScript
browser-launcher Browser-Launcher可以检测系统上的所有浏览器版本,并在一个独立的配置文件中启动它们,用于自动测试。 JavaScript
browser.rb 无界面 Ruby 浏览器。 Ruby
Browserjet 无界面webkit浏览器,采用node.js接口。 JavaScript
BrowserKit 可模拟浏览器的行为。 PHP
CasperJS CasperJS 是一个开源的导航脚本和测试工具,使用 JavaScript 基于 PhantomJS 编写,用于测试 Web 应用功能,Phantom JS是一个服务器端的 JavaScript API 的 WebKit。其支持各种Web标准: DOM 处理, CSS 选择器, JSON, Canvas, 和 SVG。 JavaScript
DalekJS DalekJS 是一个基于 JavaScript(或 Node.js) 的免费和开源的自动化测试接口。它能够同时运行测试一组流行的浏览器(Chrome,IE,Firefox 和 WebKit)。 JavaScript
Erik Erik是一款基于WebKit的无界面浏览器,可用于功能函数的测试,使用JavaScript对网页进行操作访问。 Swift
Geb Geb 是浏览器自动化(browser automation)测试解決方案。 Groovy
ghost.py ghost.py 是一个 Python 的 Webkit 的 Web 客户端。 Python
Ghostbuster Ghostbuster 是一款自动化浏览器测试工具,基于phantomjs,意味着你得到一个仿真浏览器,一个真正的DOM,仿真测试环境。 JavaScript
grope Grope 是无GUI浏览器环境,使用WebKit Framework + RubyCocoa。 Ruby
Guillotine Guillotine 是一款采用C#开发的.NET 无界面浏览器。 .NET
Headless Headless是一款无界面浏览器,支持快速网络接受测试,采用.Net环境。 .NET
headless_browser Headless-Browser 是一款采用C++开发的基于WebKit 无界面浏览器。 C++
HeadlessBrowser HeadlessBrowser是一款轻量级无图形界面浏览器,用于DOM测试。 JavaScript
HtmlUnit HtmlUnit 是一个is a “Java 程序 GUI-Less 浏览器”。 Java
Jabba-Webkit Jabba-Webkit是一款无图形化 WebKit 浏览器,主要用来抓取Ajax网页。 Python
Jasmine-Headless-Webkit Jasmine-Headless-Webkit是一款基于jasmine的无图形化web工具。 Python, JavaScript, Ruby
Jaunt Java Web 网页抓取&自动化 API Java
jBrowserDriver jBrowserDriver是一款采用纯Java编写的无图形化浏览器,基于WebKit,和Selenium兼容。 Java
jedi-crawler Jedi-Crawler 是一款轻量级 Node/PhantomJS爬虫,可以动态的抓取网页内容。 JavaScript
Lotte Lotte是一款自动化无图形化浏览器测试工具,采用phantomJs。 JavaScript
MechanicalSoup MechanicalSoup是一个与网站自动交互Python库。 Python
mechanize 状态编程的Web浏览。 Python
Nightmare 高层次浏览器自动化库,构建于PhantomJS。 JavaScript
PhantomJS Phantom JS是一个服务器端的 JavaScript API 的 WebKit JavaScript, Python, Ruby, Java, C#, Haskell, Objective-C, Perl, PHP, R(via Selenium)
phantompy Phantompy 是一款headless WebKit 引擎,构建于强大的 Qt5 Webkit API之上。 Python
Python-Webkit Python-Webkit 是一个Webkit python扩展, 可完整的访问网页的DOM。 Python
RoboBrowser RoboBrowser 是一款简单的浏览网页的Pythonic库,无需依赖独立的浏览器。 Python
Selenium 跨平台自动化web浏览器。 JavaScript, Python, Ruby, Java, C#, Haskell, Objective-C, Perl, PHP, R
SimpleBrowser SimpleBrowser是专门为自动化任务而设计的一个灵活而直观的浏览器引擎,内置.Net 4 framework。 .NET
SlimerJS SlimerJS 是一个提供给 Web 开发人员,可通过脚本编程控制的浏览器。 JavaScript
Splash Splash是一款HTTP API 轻量级浏览器,采用Python和QT开发。 Any
Splinter Splinter 是一个用 Python 编写的 Web 应用程序进行验收测试的工具。 Python
Spynner Spynner是一个可编程Web浏览器Python模块。支持AJAX Python
SST SST (selenium-simple-test) 是一个 Web 测试框架,使用 Python 来生成基于浏览器的功能测试。 Python
stanislaw Stanislaw一款Python headless 浏览器测试工具。 Python
trifleJS 一个 headless IE 浏览器。采用 .NET WebBrowser类,拥有Javascript API,运行在 V8引擎。 JavaScript
twill Twill是一种简单的语言,允许用户通过一个命令行界面浏览网页。 Python
WatiN Watin是一个面向.net的Web自动化测试开源项目,对应Web元素提供了丰富的类库,而且使用起来非常简单。 .NET
Watir-WebDriver Watir的实现基于WebDriver的Ruby绑定。 Ruby
WKZombie WKZombie是针对iOS/ OSX的不需要用户界面或API就能进行网站导航和数据收集的一个Swift框架,也被称为无界面浏览器。 Swift
Zombie.js 一个轻量级的框架,用于在一个模拟的环境中测试客户端的 JavaScript 代码。Zombie.js 使用 Node.js 实现快速的 headless full-stack 测试平台。 JavaScript

猜你喜欢

转载自blog.csdn.net/weixin_33768153/article/details/81183577