做一个法外狂徒:前端不死水印的对抗方法

有时候企业不希望内网页面上的内容被截图泄漏出去,或者希望在事情闹大之后追踪到在产品上截图的员工 / 用户,这时候前端水印就派上用场了。使用纯前端添加的水印大抵有这么几种形式

  • 明(文)水印
    • 一类是非常直白的肉眼可见的水印,一般是为了警告用户这个页面包含敏感内容,不要截图分享
    • 还有一些隐蔽性更强的水印,只会放在页面上部分关键区域,通过降低对比度隐藏自己,这种水印一般事后用于追溯

https://oss.sem.ms/img/watermark-example.png

  • 暗水印
    • 前端使用的暗水印通常不是通过隐写的方式如 LSB、频域水印等,这样开销会比较大,此外也只能对单个图片生效,不能覆盖整个页面
    • 一般使用非肉眼可辨识的图层重复覆盖整个页面,需要通过算法逆向提取出其中的水印内容,各家算法可能不一样

不过放在前端的水印都是比较容易被去除的,只要在浏览器 Devtools 中找到水印所在 DOM 元素删除即可。于是聪明的前端开发们使用 MutationObserver 造出了不死水印:只要水印被删除或者样式被修改,就会重新创建一个水印。

以某个国产水印库为例,不死水印的原理如下(有简化)

//监听dom是否被移除或者改变属性的回调函数
var domChangeCallback = function (records) {
  // ...
  if ((globalSetting && records.length === 1) || (records.length === 1 && records[0].removedNodes.length >= 1)) {
    // 如果被删除,则重新创建水印图层
    loadMark(globalSetting)
  }
}
 
var hasObserver = MutationObserver !== undefined
var watermarkDom = hasObserver ? new MutationObserver(domChangeCallback) : null
var option = {
  childList: true, // 防止被删除,水印外层需要额外包一层 div
  attributes: true, // 防止 style 被修改
  subtree: true, // 防止被删除
}
 
// ...
watermarkDom.observe(document.getElementById(defaultSettings.watermark_id).shadowRoot, option)
//监听dom是否被移除或者改变属性的回调函数
var domChangeCallback = function (records) {
  // ...
  if ((globalSetting && records.length === 1) || (records.length === 1 && records[0].removedNodes.length >= 1)) {
    // 如果被删除,则重新创建水印图层
    loadMark(globalSetting)
  }
}
 
var hasObserver = MutationObserver !== undefined
var watermarkDom = hasObserver ? new MutationObserver(domChangeCallback) : null
var option = {
  childList: true, // 防止被删除,水印外层需要额外包一层 div
  attributes: true, // 防止 style 被修改
  subtree: true, // 防止被删除
}
 
// ...
watermarkDom.observe(document.getElementById(defaultSettings.watermark_id).shadowRoot, option)

当水印被删除 (childList) 或者 style 被修改 (attributes) 时,MutationObserver 就会触发回调,重新绘制水印,让我们无法通过正常方式删除。当然还有一些变种的不死水印,在被删除 / 修改时会清空页面或者后台上报删除事件,让人防不胜防。对付这些水印,我们可以在控制台上禁用 JavaScript 来防止这些骚操作,然后尽情地去除水印再进行截图。

不过这样还是有点麻烦,如果想要一个没有水印的干净清爽的世界,有更好的方法吗?

既然不死水印使用的是 MutationObserver,我们可以通过油猴脚本,我们可以在页面脚本之前把这个东西干掉。

// ==UserScript==
// @run-at       document-start
// ==/UserScript==
 
delete window.MutationObserver
delete window.WebKitMutationObserver
delete window.MozMutationObserver
// ==UserScript==
// @run-at       document-start
// ==/UserScript==
 
delete window.MutationObserver
delete window.WebKitMutationObserver
delete window.MozMutationObserver

大公告成!不过有些站点本身就会使用 MutationObserver ,贸然删除会导致功能不可用,如果要精准控制的话就需要自己写 Proxy 劫持 construct 来处理了,这样又会麻烦起来。那么有没有一劳永逸的方法呢?

其实还是有的,也就是本文的正题了——

水印制作者需要花费很大力气保证水印可见并且可以恢复出信息,而去除水印只需要让它不可见就可以了。那么有没有办法在不修改水印本身样式的情况下让它不可见呢?答案就是使用 CSS 从“旁路”攻击水印。

假设我们有一个水印图层长这样

<div id="wm_div_id" style="pointer-events: none !important; display: block !important"></div>
<div id="wm_div_id" style="pointer-events: none !important; display: block !important"></div>

虽然水印已经内联了 important 样式防止被 display: none 掉,但我们还有非常多的方式可以让它不可见:

#wm_div_id {
  /* 通过使其透明让它不可见 */
  opacity: 0;
 
  /* 修改可见性为 hidden 让它不可见 */
  visibility: hidden;
 
  /* 让它尺寸为 0 不可见 */
  width: 0;
  height: 0;
 
  /* 设置它的位置在屏幕之外让它不可见 */
  position: absolute;
  top: -9999;
 
  /* 让它被背景遮住不可见 */
  z-index: -100;
}
#wm_div_id {
  /* 通过使其透明让它不可见 */
  opacity: 0;
 
  /* 修改可见性为 hidden 让它不可见 */
  visibility: hidden;
 
  /* 让它尺寸为 0 不可见 */
  width: 0;
  height: 0;
 
  /* 设置它的位置在屏幕之外让它不可见 */
  position: absolute;
  top: -9999;
 
  /* 让它被背景遮住不可见 */
  z-index: -100;
}

让水印不可见并不是我们对水印元素做了任何修改,我们只是让 CSS selector 命中了这个元素并且让它看不见而已。不管是透明还是移动到屏幕之外,只要在可视区域内看不到这个元素,我们的目标就达成了。计算样式变化是不会导致 MutationObserver 触发任何回调的,而目前的 Web 规范中也暂时没有能监听计算样式变化的 API,好耶!

出于某些原因我不可以传授去水印的代码,但自己编写一个并不是什么困难的事——只要让 CSS 选择器能够选择到目标元素即可。

此外前端生成的水印图层常常会有很明显的特征可以被 CSS selector 选中,发动一点小脑筋1很快就可以写出一个能处理掉大部分站点水印的样式规则,再使用 stylish 之类的浏览器插件加载到每个网页上,这下我们可以在完全不被水印影响的情况下尽情冲浪了。

尽管 CSS 样式攻击很难防御,JS 也不是没有应对方法:Element.getComputedStyle() 可以获取特定元素的计算样式,JS 获取到计算样式之后可以检查是否被修改再重新绘制水印。获取计算样式由于会触发 RecalculateStyle 和 Layout 所以对性能可能会有影响(不过可以在 rAF 里面处理)。这样做的成本真的很高,应该不会真的有人这么做吧.jpg

相信大家已经完全掌握了这门技术,学会编写自己的攻击样式了吧。我可没有传授什么去内网水印的教程(

冲浪愉快2 :)


  1. 水印图层通常会有类似 wm watermark 字样的属性,合理运用 attribute selector 可以事半功倍

  2. 据我所知,目前也有一些公司在实验其他更隐蔽更难以去除的水印,例如基于字形/字符间距变化的水印,谨慎冲浪 :(

Loading New Comments...