前端水印破解成本的循序渐进

513 阅读3分钟

前端水印破解成本的循序渐进

起因

文心一言 的体验账号开通了,拿到账号后试了下AI图片生成觉得挺有意思的, 想分享,但碍于水印(虽然用户规则没提及不能分享)的存在,然后就学习和研究了下页面里的水印技术(如何破解)。

顺便总结了下从 增加用户破解成本 的角度来看水印的实现方案。为什么要从 从用户破解成本 的角度看水印的技术演进,个人觉得技术的攻防没有尽头,在合适的成本下我们只需要让绝大部分用户 知难而退 就够了。

文心页面水印的实现方案

MutationObserver(监听DOM元素的变动) + shadowRoot(隐藏DOM,但是里面使用的是文本渲染的方式,用base64图片会更好) + Disable debugger(写完文章后想录制GIF才发现页面新增了这个策略)

无法提供对应(删除水印后会重新加回节点)的GIF了,o(╥﹏╥)o,写这篇文章的时候页面还没升级控制台 禁止调试 策略,想着后再录,结果。。。

WechatIMG62.jpeg

水印实现及破解成本的循序渐进

background-repeat

场景: 主动对用户进行弱提示

<!DOCTYPE html>
    <head>
        <title>background-repeat</title>
        <style>
            html {
                background-image: url('水印.svg');
                background-repeat: repeat;
            }
        </style>
    </head>
    <body>this is Content</body>
</html>

通过上面的代码,我们简单的在页面背景引入水印图片实现了一个带有水印的页面。

★ ★ shadowDom

这个是在文心页面中发现的,个人认为实际的作用应该是为了隔离,同时起到混淆隐藏水印节点作用, 不过隐藏节点的实现最好还是用非文本节点。

首先,我们先看上面 background-repeat 样例实现的效果,由下图中可以看到这个节点几乎是显式的(用户只需简单的对dom节点选中即可发现)。

SCR-20230323-ro9.png

再来看用 shadowDom 的实现:

<!DOCTYPE html>
    <head>
        <title>shadow-dom</title>
        <style>
            html, body {
                width: 100%;
                height: 100%;
            }
            #shadow-root {
                pointer-events: none;
            }
        </style>
        <script>
            window.onload = function () {
                const rootEle = document.querySelector('#shadow-root')
                const shadowRoot = rootEle.attachShadow({ mode: 'closed' })

                shadowRoot.innerHTML = `
                <div style="position: absolute;width: 100%;height: 100%;z-index: 99999;background-image: url('data:image/svg+xml,xxxxxx');"></div>`
            }
        </script>
    </head>
    <body>
        <div>这是正文,这是正文,这是正文。</div>
        <div id="shadow-root"></div>
    </body>
</html>

实现效果:

SCR-20230330-oih.png

上图的 case 由于节点的数量比较少,看起来还是比较容易发现水印的节点。 但在实际的页面中,节点的数量是非常多的,比如下面的文心页面就比较不易发现(比较耗时):

SCR-20230330-oem.jpeg

★ ★ ★ MutationObserver 作用:通过监听水印DOM节点的变动,进一步阻止用户去掉水印的行为

先看看代码的实现:

<!DOCTYPE html>
    <head>
        <title>mutation-observer</title>
        <style>
            html, body {
                width: 100%;
                height: 100%;
            }
            #shadow-root {
                pointer-events: none;
            }
        </style>
        <script>
            window.onload = function () {
                // 生成节点
                function createWaterMarkDom () {
                    let rootEle = document.querySelector('#shadow-root')
                    console.log('rootEle', rootEle)
                    if (!rootEle) {
                        rootEle =  document.createElement('div')
                        rootEle.setAttribute('id', '#shadow-root')
                        document.body.appendChild(rootEle)
                    }
                    const shadow = rootEle.attachShadow({ mode: 'open' })
                    shadow.innerHTML = `
                    <div id="shadow" style="color: #999;font-size: 35px;position: absolute;width: 100%;height: 100%;z-index: 99999;background-image: url('data:image/svg+xml, xxxx');">水印...</div>`
                    
                }
                createWaterMarkDom()

                // 监听body下的元素变动
                const bodyNode = document.body
                const observerCallback = function(mutations, observer) {
                    // @TODO 手动delete掉,再加回来
                    console.log('mutations', mutations)
                    console.log('observer', observer)
                }
                const observerConfig = {
                    attributes: true, // 节点的特性
                    childList: true, // 节点的子节点的更改
                    subtree: true, // 节点所有后代的更改
                    characterData: true // 节点的文本内容
                }
                const bodyObserver = new MutationObserver(observerCallback)
                bodyObserver.observe(bodyNode, observerConfig)

                // 监听shadowRoot宿主元素的变动
                const shadowRootNode = document.querySelector('#shadow-root')
                const shadowRootObserver = new MutationObserver(observerCallback)
                shadowRootObserver.observe(shadowRootNode, observerConfig)

                // 监听shadow元素的变动
                const shadowNode = shadowRootNode.shadowRoot.querySelector('#shadow')
                const shadowObserver = new MutationObserver(observerCallback)
                shadowObserver.observe(shadowNode,observerConfig)
            }
        </script>
    </head>
    <body>
        <div>节点</div>
        <div id="shadow-root"></div>
        <div>其他节点</div>
        <div>这是正文,这是正文,这是正文。</div>
    </body>
</html>

看下实现效果:

SCR-20230323-tw8.png

可以看到只要对水印节点操作,就会触发 callback, 然后在回调函数里先删除水印节点,再添加回来即可。

虽然这种方式已经尽可能的增加用户去除水印的操作成本,但是通过浏览器的控制台设置,还是可以让这种方案沦陷。

SCR-20230323-u2b.png

但是这种方式在页面加载完成后,选择开启 Disable JavaScript 后,我们的 Observer 就失效了。

★ ★ ★ ★ Disable debugger

作用: 限制用户在控制台调试,阻止用户对 Disable JavaScript 的设置

(function (){
    const temp = Object.create(null)
    let t = Date.now()
    Object.defineProperty(temp, 'v', {
        get: function() {
            if(Date.now() - t > 100){
                alert('非法操作,将记录系统')
                window.location.href = "about:blank"
            }
        }
    })
    setInterval(function(){
        t = Date.now()
        debugger
        console.log(temp.v)
    }, 200)
})()

实现效果如下:点击确定将会跳转空白页,实际页面中我们打包代码会通过压缩混淆的方式,使得 source 里的代码没那么直观。

SCR-20230324-eya.png

当然还有更高级别的反调试(比如:爆栈等),反调试的攻防一山还有一山高,就不进行讨论了。在这里我们只针对 增加用户对水印的破解成本 探讨而简单举的例子。

★ ★ ★ ★ ★ 隐式水印

作用:不可见,肉眼无法识别,多用于版权保护盗用追溯等,破解成本对于普通用户较高

这里就不去实现,具体的实现可看下 不能说的秘密——前端也能玩的图片隐写术 的实现,也有第三方的服务商的比如:阿里云防泄漏数字水印服务、腾讯云盲水印服务;