整合營銷服務商

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

          免費咨詢熱線:

          「2022 年」崔慶才 Python3 爬蟲教程 - JavaScript 網站加密和混淆技術


          著大數據時代的發展,各個公司的數據保護意識越來越強,大家都在想盡辦法保護自家產品的數據不輕易被爬蟲爬走。由于網頁是提供信息和服務的重要載體,所以對網頁上的信息進行保護就成了至關重要的一個環節。

          網頁是運行在瀏覽器端的,當我們瀏覽一個網頁時,其 HTML 代碼、 JavaScript 代碼都會被下載到瀏覽器中執行。借助瀏覽器的開發者工具,我們可以看到網頁在加載過程中所有網絡請求的詳細信息,也能清楚地看到網站運行的 HTML 代碼和 JavaScript 代碼,這些代碼中就包含了網站加載的全部邏輯,如加載哪些資源、請求接口是如何構造的、頁面是如何渲染的等等。正因為代碼是完全透明的,所以如果我們能夠把其中的執行邏輯研究出來,就可以模擬各個網絡請求進行數據爬取了。

          然而,事情沒有想象得那么簡單。隨著前端技術的發展,前端代碼的打包技術、混淆技術、加密技術也層出不窮,借助于這些技術,各個公司可以在前端對 JavaScript 代碼采取一定的保護,比如變量名混淆、執行邏輯混淆、反調試、核心邏輯加密等,這些保護手段使得我們沒法很輕易地找出 JavaScript 代碼中包含的的執行邏輯。

          在前幾章的案例中,我們也試著爬取了各種形式的網站。其中有的網站的數據接口是沒有任何驗證或加密參數的,我們可以輕松模擬并爬取其中的數據;但有的網站稍顯復雜,網站的接口中增加了一些加密參數,同時對 JavaScript 代碼采取了上文所述的一些防護措施,當時我們沒有直接嘗試去破解,而是用 Selenium 等類似工具來實現模擬瀏覽器執行的方式來進行“所見即所得“的爬取。其實對于后者,我們還有另外一種解決方案,那就是直接逆向 JavaScript 代碼,找出其中的加密邏輯,從而直接實現該加密邏輯來進行爬取。如果加密邏輯實在過于復雜,我們也可以找出一些關鍵入口,從而實現對加密邏輯的單獨模擬執行和數據爬取。這些方案難度可能很大,比如關鍵入口很難尋找,或者加密邏輯難以模擬,可是一旦成功找到突破口,我們便可以不用借助于 Selenium 等工具進行整頁數據的渲染而實現數據爬取,這樣爬取效率會大幅提升。

          本章我們首先會對 JavaScript 防護技術進行介紹,然后介紹一些常用的 JavaScript 逆向技巧,包括瀏覽器工具的使用、Hook 技術、AST 技術、特殊混淆技術的處理、WebAssembly 技術的處理。了解了這些技術,我們可以更從容地應對 JavaScript 防護技術。

          1. 引入

          我們在爬取網站的時候,會遇到一些情況需要分析一些接口或 URL 信息,在這個過程中,我們會遇到各種各樣類似加密的情形,比如說:

          • 某個網站的 URL 帶有一些看不太懂的長串加密參數,要抓取就必須要懂得這些參數是怎么構造的,否則我們連完整的 URL 都構造不出來,更不用說爬取了。
          • 分析某個網站的 Ajax 接口的時候,可以看到接口的一些參數也是加密的,或者 Request Headers 里面也可能帶有一些加密參數,如果不知道這些參數的具體構造邏輯就沒法直接用程序來模擬這些 Ajax 請求。
          • 翻看網站的 JavaScript 源代碼,可以發現很多壓縮了或者看不太懂的字符,比如 JavaScript 文件名被編碼,JavaScript 的文件內容都壓縮成幾行,JavaScript 變量也被修改成單個字符或者一些十六進制的字符,導致我們不好輕易根據 JavaScript 找出某些接口的加密邏輯。

          這些情況呢,基本上都是網站為了保護其本身的一些數據不被輕易抓取而采取的一些措施,我們可以把它歸類為兩大類:

          • URL/API 參數加密
          • JavaScript 壓縮、混淆和加密

          這一節我們就來了解下這兩類技術的基本原理和一些常見的示例。知己知彼,百戰不殆,了解了這些技術的實現原理之后,我們才能更好地去逆向其中的邏輯,從而實現數據爬取。

          2. 網站數據防護方案

          當今大數據時代,數據已經變得越來越重要,網頁和 App 現在是主流的數據載體,如果其數據的 API 沒有設置任何保護措施,在爬蟲工程師解決了一些基本的反爬如封 IP、驗證碼的問題之后,那么數據還是可以被輕松爬取到的。

          那么,有沒有可能在 URL/API 層面或 JavaScript 層面也加上一層防護呢?答案是可以。

          URL/API 參數加密

          網站運營者首先想到防護措施可能是對某些數據接口的參數進行加密,比如說對某些 URL 的一些參數加上校驗碼或者把一些 id 信息進行編碼,使其變得難以閱讀或構造;或者對某些 API 請求加上一些 token、sign 等簽名,這樣這些請求發送到服務器時,服務器會通過客戶端發來的一些請求信息以及雙方約定好的秘鑰等來對當前的請求進行校驗,如果校驗通過,才返回對應數據結果。

          比如說客戶端和服務端約定一種接口校驗邏輯,客戶端在每次請求服務端接口的時候都會附帶一個 sign 參數,這個 sign 參數可能是由當前時間信息、請求的 URL、請求的數據、設備的 ID、雙方約定好的秘鑰經過一些加密算法構造而成的,客戶端會實現這個加密算法構造 sign,然后每次請求服務器的時候附帶上這個參數。服務端會根據約定好的算法和請求的數據對 sign 進行校驗,如果校驗通過,才返回對應的數據,否則拒絕響應。

          當然登錄狀態的校驗也可以看作是此類方案,比如一個 API 的調用必須要傳一個 token,這個 token 必須用戶登錄之后才能獲取,如果請求的時候不帶該 token,API 就不會返回任何數據。

          倘若沒有這種措施,那么基本上 URL 或者 API 接口是完全公開可以訪問的,這意味著任何人都可以直接調用來獲取數據,幾乎是零防護的狀態,這樣是非常危險的,而且數據也可以被輕易地被爬蟲爬取。因此對 URL/API 參數一些加密和校驗是非常有必要的。

          JavaScript 壓縮、混淆和加密

          接口加密技術看起來的確是一個不錯的解決方案,但單純依靠它并不能很好地解決問題。為什么呢?

          對于網頁來說,其邏輯是依賴于 JavaScript 來實現的,JavaScript 有如下特點:

          • JavaScript 代碼運行于客戶端,也就是它必須要在用戶瀏覽器端加載并運行。
          • JavaScript 代碼是公開透明的,也就是說瀏覽器可以直接獲取到正在運行的 JavaScript 的源碼。

          由于這兩個原因,至使 JavaScript 代碼是不安全的,任何人都可以讀、分析、復制、盜用,甚至篡改。

          所以說,對于上述情形,客戶端 JavaScript 對于某些加密的實現是很容易被找到或模擬的,了解了加密邏輯后,模擬參數的構造和請求也就是輕而易舉了,所以如果 JavaScript 沒有做任何層面的保護的話,接口加密技術基本上對數據起不到什么防護作用。

          如果你不想讓自己的數據被輕易獲取,不想他人了解 JavaScript 邏輯的實現,或者想降低被不懷好意的人甚至是黑客攻擊。那么就需要用到 JavaScript 壓縮、混淆和加密技術了。

          這里壓縮、混淆和加密技術簡述如下:

          • 代碼壓縮:即去除 JavaScript 代碼中的不必要的空格、換行等內容,使源碼都壓縮為幾行內容,降低代碼可讀性,當然同時也能提高網站的加載速度。
          • 代碼混淆:使用變量替換、字符串陣列化、控制流平坦化、多態變異、僵尸函數、調試保護等手段,使代碼變地難以閱讀和分析,達到最終保護的目的。但這不影響代碼原有功能。是理想、實用的 JavaScript 保護方案。
          • 代碼加密:可以通過某種手段將 JavaScript 代碼進行加密,轉成人無法閱讀或者解析的代碼,如借用 WebAssembly 技術,可以直接將 JavaScript 代碼用 C/C++ 實現,JavaScript 調用其編譯后形成的文件來執行相應的功能。

          下面我們對上面的技術分別予以介紹。

          3. URL/API 參數加密

          現在絕大多數網站的數據一般都是通過服務器提供的 API 來獲取的,網站或 App 可以請求某個數據 API 獲取到對應的數據,然后再把獲取的數據展示出來。但有些數據是比較寶貴或私密的,這些數據肯定是需要一定層面上的保護。所以不同 API 的實現也就對應著不同的安全防護級別,我們這里來總結下。

          為了提升接口的安全性,客戶端會和服務端約定一種接口校驗方式,一般來說會使用到各種加密和編碼算法,如 Base64、Hex 編碼,MD5、AES、DES、RSA 等對稱或非對稱加密。

          舉個例子,比如說客戶端和服務器雙方約定一個 sign 用作接口的簽名校驗,其生成邏輯是客戶端將 URL Path 進行 MD5 加密然后拼接上 URL 的某個參數再進行 Base64 編碼,最后得到一個字符串 sign,這個 sign 會通過 Request URL 的某個參數或 Request Headers 發送給服務器。服務器接收到請求后,對 URL Path 同樣進行 MD5 加密,然后拼接上 URL 的某個參數,也進行 Base64 編碼也得到了一個 sign,然后比對生成的 sign 和客戶端發來的 sign 是否是一致的,如果是一致的,那就返回正確的結果,否則拒絕響應。這就是一個比較簡單的接口參數加密的實現。如果有人想要調用這個接口的話,必須要定義好 sign 的生成邏輯,否則是無法正常調用接口的。

          當然上面的這個實現思路比較簡單,這里還可以增加一些時間戳信息增加時效性判斷,或增加一些非對稱加密進一步提高加密的復雜程度。但不管怎樣,只要客戶端和服務器約定好了加密和校驗邏輯,任何形式加密算法都是可以的。

          這里要實現接口參數加密就需要用到一些加密算法,客戶端和服務器肯定也都有對應的 SDK 實現這些加密算法,如 JavaScript 的 crypto-js,Python 的 hashlib、Crypto 等等。

          但還是如上文所說,如果是網頁的話,客戶端實現加密邏輯如果是用 JavaScript 來實現,其源代碼對用戶是完全可見的,如果沒有對 JavaScript 做任何保護的話,是很容易弄清楚客戶端加密的流程的。

          因此,我們需要對 JavaScript 利用壓縮、混淆等方式來對客戶端的邏輯進行一定程度上的保護。

          4. JavaScript 壓縮

          這個非常簡單,JavaScript 壓縮即去除 JavaScript 代碼中的不必要的空格、換行等內容或者把一些可能公用的代碼進行處理實現共享,最后輸出的結果都壓縮為幾行內容,代碼可讀性變得很差,同時也能提高網站加載速度。

          如果僅僅是去除空格換行這樣的壓縮方式,其實幾乎是沒有任何防護作用的,因為這種壓縮方式僅僅是降低了代碼的直接可讀性。如果我們有一些格式化工具可以輕松將 JavaScript 代碼變得易讀,比如利用 IDE、在線工具或 Chrome 瀏覽器都能還原格式化的代碼。

          比如這里舉一個最簡單的 JavaScript 壓縮示例,原來的 JavaScript 代碼是這樣的:

          function echo(stringA, stringB) {
            const name = "Germey";
            alert("hello " + name);
          }

          壓縮之后就變成這樣子:

          function echo(d, c) {
            const e = "Germey";
            alert("hello " + e);
          }

          可以看到這里參數的名稱都被簡化了,代碼中的空格也被去掉了,整個代碼也被壓縮成了一行,代碼的整體可讀性降低了。

          目前主流的前端開發技術大多都會利用 Webpack、Rollup 等工具進行打包,Webpack、Rollup 會對源代碼進行編譯和壓縮,輸出幾個打包好的 JavaScript 文件,其中我們可以看到輸出的 JavaScript 文件名帶有一些不規則字符串,同時文件內容可能只有幾行內容,變量名都是一些簡單字母表示。這其中就包含 JavaScript 壓縮技術,比如一些公共的庫輸出成 bundle 文件,一些調用邏輯壓縮和轉義成冗長的幾行代碼,這些都屬于 JavaScript 壓縮。另外其中也包含了一些很基礎的 JavaScript 混淆技術,比如把變量名、方法名替換成一些簡單字符,降低代碼可讀性。

          但整體來說,JavaScript 壓縮技術只能在很小的程度上起到防護作用,要想真正提高防護效果還得依靠 JavaScript 混淆和加密技術。

          5. JavaScript 混淆

          JavaScript 混淆是完全是在 JavaScript 上面進行的處理,它的目的就是使得 JavaScript 變得難以閱讀和分析,大大降低代碼可讀性,是一種很實用的 JavaScript 保護方案。

          JavaScript 混淆技術主要有以下幾種:

          • 變量混淆:將帶有含義的變量名、方法名、常量名隨機變為無意義的類亂碼字符串,降低代碼可讀性,如轉成單個字符或十六進制字符串。
          • 字符串混淆:將字符串陣列化集中放置、并可進行 MD5 或 Base64 加密存儲,使代碼中不出現明文字符串,這樣可以避免使用全局搜索字符串的方式定位到入口點。
          • 屬性加密:針對 JavaScript 對象的屬性進行加密轉化,隱藏代碼之間的調用關系。
          • 控制流平坦化:打亂函數原有代碼執行流程及函數調用關系,使代碼邏變得混亂無序。
          • 無用代碼注入:隨機在代碼中插入不會被執行到的無用代碼,進一步使代碼看起來更加混亂。
          • 調試保護:基于調試器特性,對當前運行環境進行檢驗,加入一些強制調試 debugger 語句,使其在調試模式下難以順利執行 JavaScript 代碼。
          • 多態變異:使 JavaScript 代碼每次被調用時,將代碼自身即立刻自動發生變異,變化為與之前完全不同的代碼,即功能完全不變,只是代碼形式變異,以此杜絕代碼被動態分析調試。
          • 鎖定域名:使 JavaScript 代碼只能在指定域名下執行。
          • 反格式化:如果對 JavaScript 代碼進行格式化,則無法執行,導致瀏覽器假死。
          • 特殊編碼:將 JavaScript 完全編碼為人不可讀的代碼,如表情符號、特殊表示內容等等。

          總之,以上方案都是 JavaScript 混淆的實現方式,可以在不同程度上保護 JavaScript 代碼。

          在前端開發中,現在 JavaScript 混淆主流的實現是 javascript-obfuscator (https://github.com/javascript-obfuscator/javascript-obfuscator) 和 terser (https://github.com/terser/terser) 這兩個庫,其都能提供一些代碼混淆功能,也都有對應的 Webpack 和 Rollup 打包工具的插件,利用它們我們可以非常方便地實現頁面的混淆,最終可以輸出壓縮和混淆后的 JavaScript 代碼,使得 JavaScript 代碼可讀性大大降低。

          下面我們以 javascript-obfuscator 為例來介紹一些代碼混淆的實現,了解了實現,那么自然我們就對混淆的機理有了更加深刻的認識。

          javascript-obfuscator 的官網地址為:https://obfuscator.io/,其官方介紹內容如下:

          A free and efficient obfuscator for JavaScript (including ES2017). Make your code harder to copy and prevent people from stealing your work.

          它是支持 ES8 的免費、高效的 JavaScript 混淆庫,它可以使得你的 JavaScript 代碼經過混淆后難以被復制、盜用,混淆后的代碼具有和原來的代碼一模一樣的功能。

          怎么使用呢?首先,我們需要安裝好 Node.js 12.x 版本及以上,確保可以正常使用 npm 命令,具體的安裝方式可以參考:https://setup.scrape.center/nodejs。

          接著新建一個文件夾,比如 js-obfuscate,然后進入該文件夾,初始化工作空間:

          npm init

          這里會提示我們輸入一些信息,創建一個 package.json 文件,這就完成了項目初始化了。

          接下來我們來安裝 javascript-obfuscator 這個庫:

          npm i -D javascript-obfuscator

          稍等片刻,即可看到本地 js-obfuscate 文件夾下生成了一個 node_modules 文件夾,里面就包含了 javascript-obfuscator 這個庫,這就說明安裝成功了,文件夾結構如圖所示:

          接下來我們就可以編寫代碼來實現一個混淆樣例了,如新建一個 main.js 文件,內容如下:

          const code = `
          let x = '1' + 1
          console.log('x', x)
          `;
          
          const options = {
            compact: false,
            controlFlowFlattening: true,
          };
          
          const obfuscator = require("javascript-obfuscator");
          function obfuscate(code, options) {
            return obfuscator.obfuscate(code, options).getObfuscatedCode();
          }
          console.log(obfuscate(code, options));

          在這里我們定義了兩個變量,一個是 code,即需要被混淆的代碼,另一個是混淆選項,是一個 Object。接下來我們引入了 javascript-obfuscator 這庫,然后定義了一個方法,傳入 code 和 options,來獲取混淆后的代碼,最后控制臺輸出混淆后的代碼。

          代碼邏輯比較簡單,我們來執行一下代碼:

          node main.js

          輸出結果如下:

          var _0x53bf = ["log"];
          (function (_0x1d84fe, _0x3aeda0) {
            var _0x10a5a = function (_0x2f0a52) {
              while (--_0x2f0a52) {
                _0x1d84fe["push"](_0x1d84fe["shift"]());
              }
            };
            _0x10a5a(++_0x3aeda0);
          })(_0x53bf, 0x172);
          var _0x480a = function (_0x4341e5, _0x5923b4) {
            _0x4341e5 = _0x4341e5 - 0x0;
            var _0xb3622e = _0x53bf[_0x4341e5];
            return _0xb3622e;
          };
          let x = "1" + 0x1;
          console[_0x480a("0x0")]("x", x);

          看到了吧,那么簡單的兩行代碼,被我們混淆成了這個樣子,其實這里我們就是設定了一個「控制流平坦化」的選項。整體看來,代碼的可讀性大大降低,也大大加大了 JavaScript 調試的難度。

          好,那么我們來跟著 javascript-obfuscator 走一遍,就能具體知道 JavaScript 混淆到底有多少方法了。

          注意:由于這些例子中,調用 javascript-obfuscator 進行混淆的實現是一樣的,所以下文的示例只說明 code 和 options 變量的修改,完整代碼請自行補全。

          代碼壓縮

          這里 javascript-obfuscator 也提供了代碼壓縮的功能,使用其參數 compact 即可完成 JavaScript 代碼的壓縮,輸出為一行內容。默認是 true,如果定義為 false,則混淆后的代碼會分行顯示。

          示例如下:

          const code = `
          let x = '1' + 1
          console.log('x', x)
          `;
          const options = {
            compact: false,
          };

          這里我們先把代碼壓縮 compact 選項設置為 false,運行結果如下:

          let x = "1" + 0x1;
          console["log"]("x", x);

          如果不設置 compact 或把 compact 設置為 true,結果如下:

          var _0x151c = ["log"];
          (function (_0x1ce384, _0x20a7c7) {
            var _0x25fc92 = function (_0x188aec) {
              while (--_0x188aec) {
                _0x1ce384["push"](_0x1ce384["shift"]());
              }
            };
            _0x25fc92(++_0x20a7c7);
          })(_0x151c, 0x1b7);
          var _0x553e = function (_0x259219, _0x241445) {
            _0x259219 = _0x259219 - 0x0;
            var _0x56d72d = _0x151c[_0x259219];
            return _0x56d72d;
          };
          let x = "1" + 0x1;
          console[_0x553e("0x0")]("x", x);

          可以看到單行顯示的時候,對變量名進行了進一步的混淆,這里變量的命名都變成了 16 進制形式的字符串,這是因為啟用了一些默認壓縮和混淆配置導致的。總之我們可以看到代碼的可讀性相比之前大大降低了。

          變量名混淆

          變量名混淆可以通過在 javascript-obfuscator 中配置 identifierNamesGenerator 參數實現,我們通過這個參數可以控制變量名混淆的方式,如 hexadecimal 則會替換為 16 進制形式的字符串,在這里我們可以設定如下值:

          • hexadecimal:將變量名替換為 16 進制形式的字符串,如 0xabc123。
          • mangled:將變量名替換為普通的簡寫字符,如 a、b、c 等。

          該參數的值默認為 hexadecimal。

          我們將該參數修改為 mangled 來試一下:

          const code = `
          let hello = '1' + 1
          console.log('hello', hello)
          `;
          const options = {
            compact: true,
            identifierNamesGenerator: "mangled",
          };

          運行結果如下:

          var a = ["hello"];
          (function (c, d) {
            var e = function (f) {
              while (--f) {
                c["push"](c["shift"]());
              }
            };
            e(++d);
          })(a, 0x9b);
          var b = function (c, d) {
            c = c - 0x0;
            var e = a[c];
            return e;
          };
          let hello = "1" + 0x1;
          console["log"](b("0x0"), hello);

          可以看到這里的變量命名都變成了 a、b 等形式。

          如果我們將 identifierNamesGenerator 修改為 hexadecimal 或者不設置,運行結果如下:

          var _0x4e98 = ["log", "hello"];
          (function (_0x4464de, _0x39de6c) {
            var _0xdffdda = function (_0x6a95d5) {
              while (--_0x6a95d5) {
                _0x4464de["push"](_0x4464de["shift"]());
              }
            };
            _0xdffdda(++_0x39de6c);
          })(_0x4e98, 0xc8);
          var _0x53cb = function (_0x393bda, _0x8504e7) {
            _0x393bda = _0x393bda - 0x0;
            var _0x46ab80 = _0x4e98[_0x393bda];
            return _0x46ab80;
          };
          let hello = "1" + 0x1;
          console[_0x53cb("0x0")](_0x53cb("0x1"), hello);

          可以看到選用了 mangled,其代碼體積會更小,但 hexadecimal 其可讀性會更低。

          另外我們還可以通過設置 identifiersPrefix 參數來控制混淆后的變量前綴,示例如下:

          const code = `
          let hello = '1' + 1
          console.log('hello', hello)
          `;
          const options = {
            identifiersPrefix: "germey",
          };

          運行結果如下:

          var germey_0x3dea = ["log", "hello"];
          (function (_0x348ff3, _0x5330e8) {
            var _0x1568b1 = function (_0x4740d8) {
              while (--_0x4740d8) {
                _0x348ff3["push"](_0x348ff3["shift"]());
              }
            };
            _0x1568b1(++_0x5330e8);
          })(germey_0x3dea, 0x94);
          var germey_0x30e4 = function (_0x2e8f7c, _0x1066a8) {
            _0x2e8f7c = _0x2e8f7c - 0x0;
            var _0x5166ba = germey_0x3dea[_0x2e8f7c];
            return _0x5166ba;
          };
          let hello = "1" + 0x1;
          console[germey_0x30e4("0x0")](germey_0x30e4("0x1"), hello);

          可以看到混淆后的變量前綴加上了我們自定義的字符串 germey。

          另外 renameGlobals 這個參數還可以指定是否混淆全局變量和函數名稱,默認為 false。示例如下:

          const code = `
          var $ = function(id) {
              return document.getElementById(id);
          };
          `;
          const options = {
            renameGlobals: true,
          };

          運行結果如下:

          var _0x4864b0 = function (_0x5763be) {
            return document["getElementById"](_0x5763be);
          };

          可以看到這里我們聲明了一個全局變量 $,在 renameGlobals 設置為 true 之后,$ 這個變量也被替換了。如果后文用到了這個 $ 對象,可能就會有找不到定義的錯誤,因此這個參數可能導致代碼執行不通。

          如果我們不設置 renameGlobals 或者設置為 false,結果如下:

          var _0x239a = ["getElementById"];
          (function (_0x3f45a3, _0x583dfa) {
            var _0x2cade2 = function (_0x28479a) {
              while (--_0x28479a) {
                _0x3f45a3["push"](_0x3f45a3["shift"]());
              }
            };
            _0x2cade2(++_0x583dfa);
          })(_0x239a, 0xe1);
          var _0x3758 = function (_0x18659d, _0x50c21d) {
            _0x18659d = _0x18659d - 0x0;
            var _0x531b8d = _0x239a[_0x18659d];
            return _0x531b8d;
          };
          var $ = function (_0x3d8723) {
            return document[_0x3758("0x0")](_0x3d8723);
          };

          可以看到,最后還是有 $ 的聲明,其全局名稱沒有被改變。

          字符串混淆

          字符串混淆,即將一個字符串聲明放到一個數組里面,使之無法被直接搜索到。我們可以通過控制 stringArray 參數來控制,默認為 true。

          我們還可以通過 rotateStringArray 參數來控制數組化后結果的的元素順序,默認為 true。還可以通過 stringArrayEncoding 參數來控制數組的編碼形式,默認不開啟編碼,如果設置為 true 或 base64,則會使用 Base64 編碼,如果設置為 rc4,則使用 RC4 編碼。另外可以通過 stringArrayThreshold 來控制啟用編碼的概率,范圍 0 到 1,默認 0.8。

          示例如下:

          const code = `
          var a = 'hello world'   
          `;
          const options = {
            stringArray: true,
            rotateStringArray: true,
            stringArrayEncoding: true, // 'base64' 或 'rc4' 或 false
            stringArrayThreshold: 1,
          };

          運行結果如下:

          var _0x4215 = ["aGVsbG8gd29ybGQ="];
          (function (_0x42bf17, _0x4c348f) {
            var _0x328832 = function (_0x355be1) {
              while (--_0x355be1) {
                _0x42bf17["push"](_0x42bf17["shift"]());
              }
            };
            _0x328832(++_0x4c348f);
          })(_0x4215, 0x1da);
          var _0x5191 = function (_0x3cf2ba, _0x1917d8) {
            _0x3cf2ba = _0x3cf2ba - 0x0;
            var _0x1f93f0 = _0x4215[_0x3cf2ba];
            if (_0x5191["LqbVDH"] === undefined) {
              (function () {
                var _0x5096b2;
                try {
                  var _0x282db1 = Function(
                    "return\x20(function()\x20" +
                      "{}.constructor(\x22return\x20this\x22)(\x20)" +
                      ");"
                  );
                  _0x5096b2 = _0x282db1();
                } catch (_0x2acb9c) {
                  _0x5096b2 = window;
                }
                var _0x388c14 =
                  "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
                _0x5096b2["atob"] ||
                  (_0x5096b2["atob"] = function (_0x4cc27c) {
                    var _0x2af4ae = String(_0x4cc27c)["replace"](/=+$/, "");
                    for (
                      var _0x21400b = 0x0,
                        _0x3f4e2e,
                        _0x5b193b,
                        _0x233381 = 0x0,
                        _0x3dccf7 = "";
                      (_0x5b193b = _0x2af4ae["charAt"](_0x233381++));
                      ~_0x5b193b &&
                      ((_0x3f4e2e =
                        _0x21400b % 0x4 ? _0x3f4e2e * 0x40 + _0x5b193b : _0x5b193b),
                      _0x21400b++ % 0x4)
                        ? (_0x3dccf7 += String["fromCharCode"](
                            0xff & (_0x3f4e2e >> ((-0x2 * _0x21400b) & 0x6))
                          ))
                        : 0x0
                    ) {
                      _0x5b193b = _0x388c14["indexOf"](_0x5b193b);
                    }
                    return _0x3dccf7;
                  });
              })();
              _0x5191["DuIurT"] = function (_0x51888e) {
                var _0x29801f = atob(_0x51888e);
                var _0x561e62 = [];
                for (
                  var _0x5dd788 = 0x0, _0x1a8b73 = _0x29801f["length"];
                  _0x5dd788 < _0x1a8b73;
                  _0x5dd788++
                ) {
                  _0x561e62 +=
                    "%" +
                    ("00" + _0x29801f["charCodeAt"](_0x5dd788)["toString"](0x10))[
                      "slice"
                    ](-0x2);
                }
                return decodeURIComponent(_0x561e62);
              };
              _0x5191["mgoBRd"] = {};
              _0x5191["LqbVDH"] = !![];
            }
            var _0x1741f0 = _0x5191["mgoBRd"][_0x3cf2ba];
            if (_0x1741f0 === undefined) {
              _0x1f93f0 = _0x5191["DuIurT"](_0x1f93f0);
              _0x5191["mgoBRd"][_0x3cf2ba] = _0x1f93f0;
            } else {
              _0x1f93f0 = _0x1741f0;
            }
            return _0x1f93f0;
          };
          var a = _0x5191("0x0");

          可以看到這里就把字符串進行了 Base64 編碼,我們再也無法通過查找的方式找到字符串的位置了。

          如果將 stringArray 設置為 false 的話,輸出就是這樣:

          var a = "hello\x20world";

          字符串就仍然是明文顯示的,沒有被編碼。

          另外我們還可以使用 unicodeEscapeSequence 這個參數對字符串進行 Unicode 轉碼,使之更加難以辨認,示例如下:

          const code = `
          var a = 'hello world'
          `;
          const options = {
            compact: false,
            unicodeEscapeSequence: true,
          };

          運行結果如下:

          var _0x5c0d = ["\x68\x65\x6c\x6c\x6f\x20\x77\x6f\x72\x6c\x64"];
          (function (_0x54cc9c, _0x57a3b2) {
            var _0xf833cf = function (_0x3cd8c6) {
              while (--_0x3cd8c6) {
                _0x54cc9c["push"](_0x54cc9c["shift"]());
              }
            };
            _0xf833cf(++_0x57a3b2);
          })(_0x5c0d, 0x17d);
          var _0x28e8 = function (_0x3fd645, _0x2cf5e7) {
            _0x3fd645 = _0x3fd645 - 0x0;
            var _0x298a20 = _0x5c0d[_0x3fd645];
            return _0x298a20;
          };
          var a = _0x28e8("0x0");

          可以看到,這里字符串被數字化和 Unicode 化,非常難以辨認。

          在很多 JavaScript 逆向的過程中,一些關鍵的字符串可能會作為切入點來查找加密入口。用了這種混淆之后,如果有人想通過全局搜索的方式搜索 hello 這樣的字符串找加密入口,也沒法搜到了。

          代碼自我保護

          我們可以通過設置 selfDefending 參數來開啟代碼自我保護功能。開啟之后,混淆后的 JavaScript 會以強制一行形式顯示,如果我們將混淆后的代碼進行格式化或者重命名,該段代碼將無法執行。

          示例如下:

          const code = `
          console.log('hello world')
          `;
          const options = {
            selfDefending: true,
          };

          運行結果如下:

          var _0x26da = ["log", "hello\x20world"];
          (function (_0x190327, _0x57c2c0) {
            var _0x577762 = function (_0xc9dabb) {
              while (--_0xc9dabb) {
                _0x190327["push"](_0x190327["shift"]());
              }
            };
            var _0x35976e = function () {
              var _0x16b3fe = {
                data: { key: "cookie", value: "timeout" },
                setCookie: function (_0x2d52d5, _0x16feda, _0x57cadf, _0x56056f) {
                  _0x56056f = _0x56056f || {};
                  var _0x5b6dc3 = _0x16feda + "=" + _0x57cadf;
                  var _0x333ced = 0x0;
                  for (
                    var _0x333ced = 0x0, _0x19ae36 = _0x2d52d5["length"];
                    _0x333ced < _0x19ae36;
                    _0x333ced++
                  ) {
                    var _0x409587 = _0x2d52d5[_0x333ced];
                    _0x5b6dc3 += ";\x20" + _0x409587;
                    var _0x4aa006 = _0x2d52d5[_0x409587];
                    _0x2d52d5["push"](_0x4aa006);
                    _0x19ae36 = _0x2d52d5["length"];
                    if (_0x4aa006 !== !![]) {
                      _0x5b6dc3 += "=" + _0x4aa006;
                    }
                  }
                  _0x56056f["cookie"] = _0x5b6dc3;
                },
                removeCookie: function () {
                  return "dev";
                },
                getCookie: function (_0x30c497, _0x51923d) {
                  _0x30c497 =
                    _0x30c497 ||
                    function (_0x4b7e18) {
                      return _0x4b7e18;
                    };
                  var _0x557e06 = _0x30c497(
                    new RegExp(
                      "(?:^|;\x20)" +
                        _0x51923d["replace"](/([.$?*|{}()[]\/+^])/g, "$1") +
                        "=([^;]*)"
                    )
                  );
                  var _0x817646 = function (_0xf3fae7, _0x5d8208) {
                    _0xf3fae7(++_0x5d8208);
                  };
                  _0x817646(_0x577762, _0x57c2c0);
                  return _0x557e06 ? decodeURIComponent(_0x557e06[0x1]) : undefined;
                },
              };
              var _0x4673cd = function () {
                var _0x4c6c5c = new RegExp(
                  "\x5cw+\x20*\x5c(\x5c)\x20*{\x5cw+\x20*[\x27|\x22].+[\x27|\x22];?\x20*}"
                );
                return _0x4c6c5c["test"](_0x16b3fe["removeCookie"]["toString"]());
              };
              _0x16b3fe["updateCookie"] = _0x4673cd;
              var _0x5baa80 = "";
              var _0x1faf19 = _0x16b3fe["updateCookie"]();
              if (!_0x1faf19) {
                _0x16b3fe["setCookie"](["*"], "counter", 0x1);
              } else if (_0x1faf19) {
                _0x5baa80 = _0x16b3fe["getCookie"](null, "counter");
              } else {
                _0x16b3fe["removeCookie"]();
              }
            };
            _0x35976e();
          })(_0x26da, 0x140);
          var _0x4391 = function (_0x1b42d8, _0x57edc8) {
            _0x1b42d8 = _0x1b42d8 - 0x0;
            var _0x2fbeca = _0x26da[_0x1b42d8];
            return _0x2fbeca;
          };
          var _0x197926 = (function () {
            var _0x10598f = !![];
            return function (_0xffa3b3, _0x7a40f9) {
              var _0x48e571 = _0x10598f
                ? function () {
                    if (_0x7a40f9) {
                      var _0x2194b5 = _0x7a40f9["apply"](_0xffa3b3, arguments);
                      _0x7a40f9 = null;
                      return _0x2194b5;
                    }
                  }
                : function () {};
              _0x10598f = ![];
              return _0x48e571;
            };
          })();
          var _0x2c6fd7 = _0x197926(this, function () {
            var _0x4828bb = function () {
                return "\x64\x65\x76";
              },
              _0x35c3bc = function () {
                return "\x77\x69\x6e\x64\x6f\x77";
              };
            var _0x456070 = function () {
              var _0x4576a4 = new RegExp(
                "\x5c\x77\x2b\x20\x2a\x5c\x28\x5c\x29\x20\x2a\x7b\x5c\x77\x2b\x20\x2a\x5b\x27\x7c\x22\x5d\x2e\x2b\x5b\x27\x7c\x22\x5d\x3b\x3f\x20\x2a\x7d"
              );
              return !_0x4576a4["\x74\x65\x73\x74"](
                _0x4828bb["\x74\x6f\x53\x74\x72\x69\x6e\x67"]()
              );
            };
            var _0x3fde69 = function () {
              var _0xabb6f4 = new RegExp(
                "\x28\x5c\x5c\x5b\x78\x7c\x75\x5d\x28\x5c\x77\x29\x7b\x32\x2c\x34\x7d\x29\x2b"
              );
              return _0xabb6f4["\x74\x65\x73\x74"](
                _0x35c3bc["\x74\x6f\x53\x74\x72\x69\x6e\x67"]()
              );
            };
            var _0x2d9a50 = function (_0x58fdb4) {
              var _0x2a6361 = ~-0x1 >> (0x1 + (0xff % 0x0));
              if (_0x58fdb4["\x69\x6e\x64\x65\x78\x4f\x66"]("\x69" === _0x2a6361)) {
                _0xc388c5(_0x58fdb4);
              }
            };
            var _0xc388c5 = function (_0x2073d6) {
              var _0x6bb49f = ~-0x4 >> (0x1 + (0xff % 0x0));
              if (
                _0x2073d6["\x69\x6e\x64\x65\x78\x4f\x66"]((!![] + "")[0x3]) !== _0x6bb49f
              ) {
                _0x2d9a50(_0x2073d6);
              }
            };
            if (!_0x456070()) {
              if (!_0x3fde69()) {
                _0x2d9a50("\x69\x6e\x64\u0435\x78\x4f\x66");
              } else {
                _0x2d9a50("\x69\x6e\x64\x65\x78\x4f\x66");
              }
            } else {
              _0x2d9a50("\x69\x6e\x64\u0435\x78\x4f\x66");
            }
          });
          _0x2c6fd7();
          console[_0x4391("0x0")](_0x4391("0x1"));

          如果我們將上述代碼放到控制臺,它的執行結果和之前是一模一樣的,沒有任何問題。

          如果我們將其進行格式化,然后貼到到瀏覽器控制臺里面,瀏覽器會直接卡死無法運行。這樣如果有人對代碼進行了格式化,就無法正常對代碼進行運行和調試,從而起到了保護作用。

          控制流平坦化

          控制流平坦化其實就是將代碼的執行邏輯混淆,使其變得復雜難讀。其基本思想是將一些邏輯處理塊都統一加上一個前驅邏輯塊,每個邏輯塊都由前驅邏輯塊進行條件判斷和分發,構成一個個閉環邏輯,導致整個執行邏輯十分復雜難讀。

          比如說這里有一段示例代碼:

          console.log(c);
          console.log(a);
          console.log(b);

          代碼邏輯一目了然,依次在控制臺輸出了 c、a、b 三個變量的值,但如果把這段代碼進行控制流平坦化處理后,代碼就會變成這樣:

          const s = "3|1|2".split("|");
          let x = 0;
          while (true) {
            switch (s[x++]) {
              case "1":
                console.log(a);
                continue;
              case "2":
                console.log(b);
                continue;
              case "3":
                console.log(c);
                continue;
            }
            break;
          }

          可以看到,混淆后的代碼首先聲明了一個變量 s,它的結果是一個列表,其實是 ["3", "1", "2"],然后下面通過 switch 語句對 s 中的元素進行了判斷,每個 case 都加上了各自的代碼邏輯。通過這樣的處理,一些連續的執行邏輯就被打破了,代碼被修改為一個 switch 語句,原本我們可以一眼看出的邏輯是控制臺先輸出 c,然后才是 a、b,但是現在我們必須要結合 switch 的判斷條件和對應 case 的內容進行判斷,我們很難再一眼每條語句的執行順序了,這就大大降低了代碼的可讀性。

          在 javascript-obfuscator 中我們通過 controlFlowFlattening 變量可以控制是否開啟控制流平坦化,示例如下:

          const options = {
            compact: false,
            controlFlowFlattening: true,
          };

          使用控制流平坦化可以使得執行邏輯更加復雜難讀,目前非常多的前端混淆都會加上這個選項。但啟用控制流平坦化之后,代碼的執行時間會變長,最長達 1.5 倍之多。

          另外我們還能使用 controlFlowFlatteningThreshold 這個參數來控制比例,取值范圍是 0 到 1,默認 0.75,如果設置為 0,那相當于 controlFlowFlattening 設置為 false,即不開啟控制流扁平化 。

          無用代碼注入

          無用代碼即不會被執行的代碼或對上下文沒有任何影響的代碼,注入之后可以對現有的 JavaScript 代碼的閱讀形成干擾。我們可以使用 deadCodeInjection 參數開啟這個選項,默認為 false。

          比如這里有一段代碼:

          const a = function () {
            console.log("hello world");
          };
          
          const b = function () {
            console.log("nice to meet you");
          };
          
          a();
          b();

          這里就聲明了方法 a 和 b,然后依次進行調用,分別輸出兩句話。

          但經過無用代碼注入處理之后,代碼就會變成類似這樣的結果:

          const _0x16c18d = function () {
            if (!![[]]) {
              console.log("hello world");
            } else {
              console.log("this");
              console.log("is");
              console.log("dead");
              console.log("code");
            }
          };
          const _0x1f7292 = function () {
            if ("xmv2nOdfy2N".charAt(4) !== String.fromCharCode(110)) {
              console.log("this");
              console.log("is");
              console.log("dead");
              console.log("code");
            } else {
              console.log("nice to meet you");
            }
          };
          
          _0x16c18d();
          _0x1f7292();

          可以看到,每個方法內部都增加了額外的 if else 語句,其中 if 的判斷條件還是一個表達式,其結果是 true 還是 false 我們還不太一眼能看出來,比如說 _0x1f7292 這個方法,它的 if 判斷條件是:

          "xmv2nOdfy2N".charAt(4) !== String.fromCharCode(110)

          在不等號前面其實是從字符串中取出指定位置的字符,不等號后面則調用了 fromCharCode 方法來根據 ascii 碼轉換得到一個字符,然后比較兩個字符的結果是否是不一樣的。前者經過我們推算可以知道結果是 n,但對于后者,多數情況下我們還得去查一下 ascii 碼表才能知道其結果也是 n,最后兩個結果是相同的,所以整個表達式的結果是 false,所以 if 后面跟的邏輯實際上就是不會被執行到的無用代碼,但這些代碼對我們閱讀代碼起到了一定的干擾作用。

          因此,這種混淆方式通過混入一些特殊的判斷條件并加入一些不會被執行的代碼,可以對代碼起到一定的混淆干擾作用。

          在 javascript-obfuscator 中,我們可以通過 deadCodeInjection 參數控制無用代碼的注入,配置如下:

          const options = {
            compact: false,
            deadCodeInjection: true,
          };

          另外我們還可以通過設置 deadCodeInjectionThreshold 參數來控制無用代碼注入的比例,取值 0 到 1,默認是 0.4。

          對象鍵名替換

          如果是一個對象,可以使用 transformObjectKeys 來對對象的鍵值進行替換,示例如下:

          const code = `
          (function(){
              var object = {
                  foo: 'test1',
                  bar: {
                      baz: 'test2'
                  }
              };
          })(); 
          `;
          const options = {
            compact: false,
            transformObjectKeys: true,
          };

          輸出結果如下:

          var _0x7a5d = ["bar", "test2", "test1"];
          (function (_0x59fec5, _0x2e4fac) {
            var _0x231e7a = function (_0x46f33e) {
              while (--_0x46f33e) {
                _0x59fec5["push"](_0x59fec5["shift"]());
              }
            };
            _0x231e7a(++_0x2e4fac);
          })(_0x7a5d, 0x167);
          var _0x3bc4 = function (_0x309ad3, _0x22d5ac) {
            _0x309ad3 = _0x309ad3 - 0x0;
            var _0x3a034e = _0x7a5d[_0x309ad3];
            return _0x3a034e;
          };
          (function () {
            var _0x9f1fd1 = {};
            _0x9f1fd1["foo"] = _0x3bc4("0x0");
            _0x9f1fd1[_0x3bc4("0x1")] = {};
            _0x9f1fd1[_0x3bc4("0x1")]["baz"] = _0x3bc4("0x2");
          })();

          可以看到,Object 的變量名被替換為了特殊的變量,使得可讀性變差,這樣我們就不好直接通過變量名進行搜尋了,這也可以起到一定的防護作用。

          禁用控制臺輸出

          可以使用 disableConsoleOutput 來禁用掉 console.log 輸出功能,加大調試難度,示例如下:

          const code = `
          console.log('hello world')
          `;
          const options = {
            disableConsoleOutput: true,
          };

          運行結果如下:

          var _0x3a39 = [
            "debug",
            "info",
            "error",
            "exception",
            "trace",
            "hello\x20world",
            "apply",
            "{}.constructor(\x22return\x20this\x22)(\x20)",
            "console",
            "log",
            "warn",
          ];
          (function (_0x2a157a, _0x5d9d3b) {
            var _0x488e2c = function (_0x5bcb73) {
              while (--_0x5bcb73) {
                _0x2a157a["push"](_0x2a157a["shift"]());
              }
            };
            _0x488e2c(++_0x5d9d3b);
          })(_0x3a39, 0x10e);
          var _0x5bff = function (_0x43bdfc, _0x52e4c6) {
            _0x43bdfc = _0x43bdfc - 0x0;
            var _0xb67384 = _0x3a39[_0x43bdfc];
            return _0xb67384;
          };
          var _0x349b01 = (function () {
            var _0x1f484b = !![];
            return function (_0x5efe0d, _0x33db62) {
              var _0x20bcd2 = _0x1f484b
                ? function () {
                    if (_0x33db62) {
                      var _0x77054c = _0x33db62[_0x5bff("0x0")](_0x5efe0d, arguments);
                      _0x33db62 = null;
                      return _0x77054c;
                    }
                  }
                : function () {};
              _0x1f484b = ![];
              return _0x20bcd2;
            };
          })();
          var _0x19f538 = _0x349b01(this, function () {
            var _0x7ab6e4 = function () {};
            var _0x157bff;
            try {
              var _0x5e672c = Function(
                "return\x20(function()\x20" + _0x5bff("0x1") + ");"
              );
              _0x157bff = _0x5e672c();
            } catch (_0x11028d) {
              _0x157bff = window;
            }
            if (!_0x157bff[_0x5bff("0x2")]) {
              _0x157bff[_0x5bff("0x2")] = (function (_0x7ab6e4) {
                var _0x5a8d9e = {};
                _0x5a8d9e[_0x5bff("0x3")] = _0x7ab6e4;
                _0x5a8d9e[_0x5bff("0x4")] = _0x7ab6e4;
                _0x5a8d9e[_0x5bff("0x5")] = _0x7ab6e4;
                _0x5a8d9e[_0x5bff("0x6")] = _0x7ab6e4;
                _0x5a8d9e[_0x5bff("0x7")] = _0x7ab6e4;
                _0x5a8d9e[_0x5bff("0x8")] = _0x7ab6e4;
                _0x5a8d9e[_0x5bff("0x9")] = _0x7ab6e4;
                return _0x5a8d9e;
              })(_0x7ab6e4);
            } else {
              _0x157bff[_0x5bff("0x2")][_0x5bff("0x3")] = _0x7ab6e4;
              _0x157bff[_0x5bff("0x2")][_0x5bff("0x4")] = _0x7ab6e4;
              _0x157bff[_0x5bff("0x2")]["debug"] = _0x7ab6e4;
              _0x157bff[_0x5bff("0x2")][_0x5bff("0x6")] = _0x7ab6e4;
              _0x157bff[_0x5bff("0x2")][_0x5bff("0x7")] = _0x7ab6e4;
              _0x157bff[_0x5bff("0x2")][_0x5bff("0x8")] = _0x7ab6e4;
              _0x157bff[_0x5bff("0x2")][_0x5bff("0x9")] = _0x7ab6e4;
            }
          });
          _0x19f538();
          console[_0x5bff("0x3")](_0x5bff("0xa"));

          此時,我們如果執行這個代碼,發現是沒有任何輸出的,這里實際上就是將 console 的一些功能禁用了。

          調試保護

          我們知道,在 JavaScript 代碼中如果加入 debugger 這個關鍵字,那么在執行到該位置的時候控制它就會進入斷點調試模式。如果在代碼多個位置都加入 debugger 這個關鍵字,或者定義某個邏輯來反復執行 debugger,那就會不斷進入斷點調試模式,原本的代碼無法就無法順暢地執行了。這個過程可以稱為調試保護,即通過反復執行 debugger 來使得原來的代碼無法順暢執行。

          其效果類似于執行了如下代碼:

          setInterval(() => {
            debugger;
          }, 3000);

          如果我們把這段代碼粘貼到控制臺,它就會反復地執行 debugger 語句進入斷點調試模式,從而干擾正常的調試流程。

          在 javascript-obfuscator 中可以使用 debugProtection 來啟用調試保護機制,還可以使用 debugProtectionInterval 來啟用無限 Debug ,使得代碼在調試過程中會不斷進入斷點模式,無法順暢執行,配置如下:

          const options = {
            debugProtection: true,
            debugProtectionInterval: true,
          };

          混淆后的代碼會不斷跳到 debugger 代碼的位置,使得整個代碼無法順暢執行,對 JavaScript 代碼的調試形成一定的干擾。

          域名鎖定

          我們還可以通過控制 domainLock 來控制 JavaScript 代碼只能在特定域名下運行,這樣就可以降低代碼被模擬或盜用的風險。

          示例如下:

          const code = `
          console.log('hello world')
          `;
          const options = {
            domainLock: ["cuiqingcai.com"],
          };

          這里我們使用了 domainLock 指定了一個域名叫做 cuiqingcai.com,也就是設置了一個域名白名單,混淆后的代碼結果如下:

          var _0x3203 = [
            "apply",
            "return\x20(function()\x20",
            "{}.constructor(\x22return\x20this\x22)(\x20)",
            "item",
            "attribute",
            "value",
            "replace",
            "length",
            "charCodeAt",
            "log",
            "hello\x20world",
          ];
          (function (_0x2ed22c, _0x3ad370) {
            var _0x49dc54 = function (_0x53a786) {
              while (--_0x53a786) {
                _0x2ed22c["push"](_0x2ed22c["shift"]());
              }
            };
            _0x49dc54(++_0x3ad370);
          })(_0x3203, 0x155);
          var _0x5b38 = function (_0xd7780b, _0x19c0f2) {
            _0xd7780b = _0xd7780b - 0x0;
            var _0x2d2f44 = _0x3203[_0xd7780b];
            return _0x2d2f44;
          };
          var _0x485919 = (function () {
            var _0x5cf798 = !![];
            return function (_0xd1fa29, _0x2ed646) {
              var _0x56abf = _0x5cf798
                ? function () {
                    if (_0x2ed646) {
                      var _0x33af63 = _0x2ed646[_0x5b38("0x0")](_0xd1fa29, arguments);
                      _0x2ed646 = null;
                      return _0x33af63;
                    }
                  }
                : function () {};
              _0x5cf798 = ![];
              return _0x56abf;
            };
          })();
          var _0x67dcc8 = _0x485919(this, function () {
            var _0x276a31;
            try {
              var _0x5c8be2 = Function(_0x5b38("0x1") + _0x5b38("0x2") + ");");
              _0x276a31 = _0x5c8be2();
            } catch (_0x5f1c00) {
              _0x276a31 = window;
            }
            var _0x254a0d = function () {
              return {
                key: _0x5b38("0x3"),
                value: _0x5b38("0x4"),
                getAttribute: (function () {
                  for (var _0x5cc3c7 = 0x0; _0x5cc3c7 < 0x3e8; _0x5cc3c7--) {
                    var _0x35b30b = _0x5cc3c7 > 0x0;
                    switch (_0x35b30b) {
                      case !![]:
                        return (
                          this[_0x5b38("0x3")] +
                          "_" +
                          this[_0x5b38("0x5")] +
                          "_" +
                          _0x5cc3c7
                        );
                      default:
                        this[_0x5b38("0x3")] + "_" + this[_0x5b38("0x5")];
                    }
                  }
                })(),
              };
            };
            var _0x3b375a = new RegExp("[QLCIKYkCFzdWpzRAXMhxJOYpTpYWJHPll]", "g");
            var _0x5a94d2 = "cuQLiqiCInKYkgCFzdWcpzRAaXMi.hcoxmJOYpTpYWJHPll"
              [_0x5b38("0x6")](_0x3b375a, "")
              ["split"](";");
            var _0x5c0da2;
            var _0x19ad5d;
            var _0x5992ca;
            var _0x40bd39;
            for (var _0x5cad1 in _0x276a31) {
              if (
                _0x5cad1[_0x5b38("0x7")] == 0x8 &&
                _0x5cad1[_0x5b38("0x8")](0x7) == 0x74 &&
                _0x5cad1[_0x5b38("0x8")](0x5) == 0x65 &&
                _0x5cad1[_0x5b38("0x8")](0x3) == 0x75 &&
                _0x5cad1[_0x5b38("0x8")](0x0) == 0x64
              ) {
                _0x5c0da2 = _0x5cad1;
                break;
              }
            }
            for (var _0x29551 in _0x276a31[_0x5c0da2]) {
              if (
                _0x29551[_0x5b38("0x7")] == 0x6 &&
                _0x29551[_0x5b38("0x8")](0x5) == 0x6e &&
                _0x29551[_0x5b38("0x8")](0x0) == 0x64
              ) {
                _0x19ad5d = _0x29551;
                break;
              }
            }
            if (!("~" > _0x19ad5d)) {
              for (var _0x2b71bd in _0x276a31[_0x5c0da2]) {
                if (
                  _0x2b71bd[_0x5b38("0x7")] == 0x8 &&
                  _0x2b71bd[_0x5b38("0x8")](0x7) == 0x6e &&
                  _0x2b71bd[_0x5b38("0x8")](0x0) == 0x6c
                ) {
                  _0x5992ca = _0x2b71bd;
                  break;
                }
              }
              for (var _0x397f55 in _0x276a31[_0x5c0da2][_0x5992ca]) {
                if (
                  _0x397f55["length"] == 0x8 &&
                  _0x397f55[_0x5b38("0x8")](0x7) == 0x65 &&
                  _0x397f55[_0x5b38("0x8")](0x0) == 0x68
                ) {
                  _0x40bd39 = _0x397f55;
                  break;
                }
              }
            }
            if (!_0x5c0da2 || !_0x276a31[_0x5c0da2]) {
              return;
            }
            var _0x5f19be = _0x276a31[_0x5c0da2][_0x19ad5d];
            var _0x674f76 =
              !!_0x276a31[_0x5c0da2][_0x5992ca] &&
              _0x276a31[_0x5c0da2][_0x5992ca][_0x40bd39];
            var _0x5e1b34 = _0x5f19be || _0x674f76;
            if (!_0x5e1b34) {
              return;
            }
            var _0x593394 = ![];
            for (var _0x479239 = 0x0; _0x479239 < _0x5a94d2["length"]; _0x479239++) {
              var _0x19ad5d = _0x5a94d2[_0x479239];
              var _0x112c24 = _0x5e1b34["length"] - _0x19ad5d["length"];
              var _0x51731c = _0x5e1b34["indexOf"](_0x19ad5d, _0x112c24);
              var _0x173191 = _0x51731c !== -0x1 && _0x51731c === _0x112c24;
              if (_0x173191) {
                if (
                  _0x5e1b34["length"] == _0x19ad5d[_0x5b38("0x7")] ||
                  _0x19ad5d["indexOf"](".") === 0x0
                ) {
                  _0x593394 = !![];
                }
              }
            }
            if (!_0x593394) {
              data;
            } else {
              return;
            }
            _0x254a0d();
          });
          _0x67dcc8();
          console[_0x5b38("0x9")](_0x5b38("0xa"));

          這段代碼就只能在指定域名 cuiqingcai.com 下運行,不能在其他網站運行。這樣的話,如果一些相關 JavaScript 代碼被單獨剝離出來,想在其他網站運行或者使用程序模擬運行的話,運行結果只有是失敗,這樣就可以有效降低被代碼被模擬或盜用的風險。

          特殊編碼

          另外還有一些特殊的工具包,如使用 aaencode、jjencode、jsfuck 等工具對代碼進行混淆和編碼。

          示例如下:

          var a = 1

          jsfuck 的結果:

          [][(![]+[])[!+[]+!![]+!![]]+([]+{})[+!![]]+(!![]+[])[+!![]]+(!![]+[])[+[]]][([]+{})[!+[]+!![]+!![]+!![]+!![]]+([]+{})[+!![]]+([][[]]+[])[+!![]]+(![]+[])[!+[]+!![]+!![]]+(!![]+[])[+[]]+(!![]+[])[+!![]]+([][[]]+[])[+[]]+([]+{})[!+[]+!![]+!![]+!![]+!![]]+(!![]+[])[+[]]+([]+{})[+!![]]+(!![]+[])[+!![]]]([][(![]+[])[!+[]+!![]+!![]]+([]+{})[+!![]]+(!![]+[])[+!![]]+(!![]+[])[+[]]][([]+{})[!+[]+!![]+!![]+!![]+!![]]+([]+{})[+!![]]+([][[]]+[])[+!![]]+
          ...
          ([]+{})[+!![]]+(!![]+[])[+!![]]]((!![]+[])[+!![]]+([][[]]+[])[!+[]+!![]+!![]]+(!![]+[])[+[]]+([][[]]+[])[+[]]+(!![]+[])[+!![]]+([][[]]+[])[+!![]]+([]+{})[!+[]+!![]+!![]+!![]+!![]+!![]+!![]]+(![]+[])[!+[]+!![]]+([]+{})[+!![]]+([]+{})[!+[]+!![]+!![]+!![]+!![]]+(+{}+[])[+!![]]+(!![]+[])[+[]]+([][[]]+[])[!+[]+!![]+!![]+!![]+!![]]+([]+{})[+!![]]+([][[]]+[])[+!![]])(!+[]+!![]+!![]+!![]+!![]))[!+[]+!![]+!![]]+([][[]]+[])[!+[]+!![]+!![]])(!+[]+!![]+!![]+!![]+!![])(([]+{})[+[]])[+[]]+(!+[]+!![]+!![]+[])+([][[]]+[])[!+[]+!![]])+([]+{})[!+[]+!![]+!![]+!![]+!![]+!![]+!![]]+(+!![]+[]))(!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![])

          aaencode 的結果:

          ?ω??= /`m′)? ~┻━┻   / ['_']; o=(???)  =_=3; c=(?Θ?) =(???)-(???); (?Д?) =(?Θ?)= (o^_^o)/ (o^_^o);(?Д?)={?Θ?: '_' ,?ω?? : ((?ω??==3) +'_') [?Θ?] ,???? :(?ω??+ '_')[o^_^o -(?Θ?)] ,?Д??:((???==3) +'_')[???] }; (?Д?) [?Θ?] =((?ω??==3) +'_') [c^_^o];(?Д?) ['c'] = ((?Д?)+'_') [ (???)+(???)-(?Θ?) ];(?Д?) ['o'] = ((?Д?)+'_') [?Θ?];(?o?)=(?Д?) ['c']+(?Д?) ['o']+(?ω?? +'_')[?Θ?]+ ((?ω??==3) +'_') [???] + ((?Д?) +'_') [(???)+(???)]+ ((???==3) +'_') [?Θ?]+((???==3) +'_') [(???) - (?Θ?)]+(?Д?) ['c']+((?Д?)+'_') [(???)+(???)]+ (?Д?) ['o']+((???==3) +'_') [?Θ?];(?Д?) ['_'] =(o^_^o) [?o?] [?o?];(?ε?)=((???==3) +'_') [?Θ?]+ (?Д?) .?Д??+((?Д?)+'_') [(???) + (???)]+((???==3) +'_') [o^_^o -?Θ?]+((???==3) +'_') [?Θ?]+ (?ω?? +'_') [?Θ?]; (???)+=(?Θ?); (?Д?)[?ε?]='\'; (?Д?).?Θ??=(?Д?+ ???)[o^_^o -(?Θ?)];(o???o)=(?ω?? +'_')[c^_^o];(?Д?) [?o?]='\"';(?Д?) ['_'] ( (?Д?) ['_'] (?ε?+(?Д?)[?o?]+ (?Д?)[?ε?]+(?Θ?)+ ((o^_^o) +(o^_^o))+ ((o^_^o) +(o^_^o))+ (?Д?)[?ε?]+(?Θ?)+ (???)+ (?Θ?)+ (?Д?)[?ε?]+(?Θ?)+ ((o^_^o) +(o^_^o))+ ((o^_^o) - (?Θ?))+ (?Д?)[?ε?]+(???)+ (c^_^o)+ (?Д?)[?ε?]+(?Θ?)+ (???)+ (?Θ?)+ (?Д?)[?ε?]+(???)+ (c^_^o)+ (?Д?)[?ε?]+((???) + (o^_^o))+ ((???) + (?Θ?))+ (?Д?)[?ε?]+(???)+ (c^_^o)+ (?Д?)[?ε?]+((o^_^o) +(o^_^o))+ (?Θ?)+ (?Д?)[?o?])(?Θ?))((?Θ?)+(?Д?)[?ε?]+((???)+(?Θ?))+(?Θ?)+(?Д?)[?o?]);

          jjencode 的結果:

          $=~[];$={___:++$,$$$$:(![]+"")[$],__$:++$,$_$_:(![]+"")[$],_$_:++$,$_$$:({}+"")[$],$$_$:($[$]+"")[$],_$$:++$,$$$_:(!""+"")[$],$__:++$,$_$:++$,$$__:({}+"")[$],$$_:++$,$$$:++$,$___:++$,$__$:++$};$.$_=($.$_=$+"")[$.$_$]+($._$=$.$_[$.__$])+($.$$=($.$+"")[$.__$])+((!$)+"")[$._$$]+($.__=$.$_[$.$$_])+($.$=(!""+"")[$.__$])+($._=(!""+"")[$._$_])+$.$_[$.$_$]+$.__+$._$+$.$;$.$$=$.$+(!""+"")[$._$$]+$.__+$._+$.$+$.$$;$.$=($.___)[$.$_][$.$_];$.$($.$($.$$+"\""+"\"+$.__$+$.$$_+$.$$_+$.$_$_+"\"+$.__$+$.$$_+$._$_+"\"+$.$__+$.___+$.$_$_+"\"+$.$__+$.___+"=\"+$.$__+$.___+$.__$+"\"")())();

          可以看到,通過這些工具,原本非常簡單的代碼被轉化為一些幾乎完全不可讀的代碼,但實際上運行效果還是相同的。這些混淆方式比較另類,看起來雖然沒有什么頭緒,但實際上找到規律是非常好還原的,其沒有真正達到強力混淆的效果。

          以上便是對 JavaScript 混淆方式的介紹和總結。總的來說,經過混淆的 JavaScript 代碼其可讀性大大降低,同時防護效果也大大增強。

          6. WebAssembly

          隨著技術的發展,WebAssembly 逐漸流行起來。不同于 JavaScript 混淆技術, WebAssembly 其基本思路是將一些核心邏輯使用其他語言(如 C/C++ 語言)來編寫,并編譯成類似字節碼的文件,并通過 JavaScript 調用執行,從而起到二進制級別的防護作用。

          WebAssembly 是一種可以使用非 JavaScript 編程語言編寫代碼并且能在瀏覽器上運行的技術方案,比如借助于我們能將 C/C++ 利用 Emscripten 編譯工具轉成 wasm 格式的文件, JavaScript 可以直接調用該文件執行其中的方法。

          WebAssembly 是經過編譯器編譯之后的字節碼,可以從 C/C++ 編譯而來,得到的字節碼具有和 JavaScript 相同的功能,運行速度更快,體積更小,而且在語法上完全脫離 JavaScript,同時具有沙盒化的執行環境。

          比如這就是一個基本的 WebAssembly 示例:

          WebAssembly.compile(
            new Uint8Array(
              `
            00 61 73 6d  01 00 00 00  01 0c 02 60  02 7f 7f 01
            7f 60 01 7f  01 7f 03 03  02 00 01 07  10 02 03 61
            64 64 00 00  06 73 71 75  61 72 65 00  01 0a 13 02
            08 00 20 00  20 01 6a 0f  0b 08 00 20  00 20 00 6c
            0f 0b`
                .trim()
                .split(/[\s\r\n]+/g)
                .map((str) => parseInt(str, 16))
            )
          ).then((module) => {
            const instance = new WebAssembly.Instance(module);
            const { add, square } = instance.exports;
            console.log("2 + 4 =", add(2, 4));
            console.log("3^2 =", square(3));
            console.log("(2 + 5)^2 =", square(add(2 + 5)));
          });

          這里其實是利用 WebAssembly 定義了兩個方法,分別是 add 和 square,可以分別用于求和和開平方計算。那這兩個方法在哪里聲明的呢?其實它們被隱藏在了一個 Uint8Array 里面,僅僅查看明文代碼我們確實無從知曉里面究竟定義了什么邏輯,但確實是可以執行的,我們將這段代碼輸入到瀏覽器控制臺下,運行結果如下:

          2 + 4 = 6
          3^2 = 9
          (2 + 5)^2 = 49

          由此可見,通過 WebAssembly 我們可以成功將核心邏輯“隱藏”起來,這樣某些核心邏輯就不能被輕易找出來了。

          所以,很多網站越來越多使用 WebAssembly 技術來保護一些核心邏輯不被輕易被人識別或破解,可以起到更好的防護效果。

          7. 總結

          以上,我們就介紹了接口加密技術和 JavaScript 的壓縮、混淆技術,也對 WebAssembly 技術有了初步的了解,知己知彼方能百戰不殆,了解了原理,我們才能更好地去實現 JavaScript 的逆向。

          本節代碼:https://github.com/Python3WebSpider/JavaScriptObfuscate。

          由于本節涉及一些專業名詞,部分內容參考來源如下:

          • GitHub - javascript-obfuscator 官方 GitHub 倉庫:https://github.com/javascript-obfuscator/javascript-obfuscator
          • 官網 - javascript-obfuscator 官網:https://obfuscator.io/
          • 博客 - asm.js 和 Emscripten 入門教程:https://www.ruanyifeng.com/blog/2017/09/asmjs_emscripten.html
          • 博客 - JavaScript 混淆安全加固:https://juejin.im/post/5cfcb9d25188257e853fa71c

          作者 壇賬號: 李恒道


          前言

          感謝videohelp論壇larley大神的解答!
          感謝吾愛破解論壇@濤之雨大神的幫助

          正文

          首先第一層是標準的OB加密
          我們先大概規整一下代碼

           復制代碼 隱藏代碼
          ? ? traverse(ast, {
          ? ?? ???CallExpression(path) {
          ? ?? ?? ?? ?if (path.node.arguments.length === 2) {
          ? ?? ?? ?? ?? ? const type0 = path.node.arguments[0].type
          ? ?? ?? ?? ?? ? const type1 = path.node.arguments[1].type
          ? ?? ?? ?? ?? ? const isLikelyNumber = (type) => {
          ? ?? ?? ?? ?? ?? ???return type === 'UnaryExpression' || type === 'NumericLiteral'
          ? ?? ?? ?? ?? ? }
          ? ?? ?? ?? ?? ? if ((type0 === 'StringLiteral' isLikelyNumber(type1)) || (type1 === 'StringLiteral' isLikelyNumber(type0))) {
          ? ?? ?? ?? ?? ?? ???const funcBinding = path.scope.getBinding(path.node.callee.name)
          ? ?? ?? ?? ?? ?? ???const funcNode = funcBinding.path.node
          ? ?? ?? ?? ?? ?? ???if (funcNode?.params?.length !== 2) {
          ? ?? ?? ?? ?? ?? ?? ?? ?return
          ? ?? ?? ?? ?? ?? ???}
          ? ?? ?? ?? ?? ?? ???if (funcNode.body.body.length !== 1) {
          ? ?? ?? ?? ?? ?? ?? ?? ?return
          ? ?? ?? ?? ?? ?? ???}
          ? ?? ?? ?? ?? ?? ???if (funcNode.body.body[0].type !== 'ReturnStatement') {
          ? ?? ?? ?? ?? ?? ?? ?? ?return
          ? ?? ?? ?? ?? ?? ???}
          ? ?? ?? ?? ?? ?? ???const funcArgs0 = funcNode.params[0].name
          ? ?? ?? ?? ?? ?? ???const funcArgs1 = funcNode.params[1].name
          ? ?? ?? ?? ?? ?? ???const bodyCallArgs = funcNode.body.body[0].argument.arguments
          ? ?? ?? ?? ?? ?? ???let isSwap = false
          ? ?? ?? ?? ?? ?? ???for (let index = 0; index < bodyCallArgs.length; index++) {
          ? ?? ?? ?? ?? ?? ?? ?? ?const item = bodyCallArgs[index];
          ? ?? ?? ?? ?? ?? ?? ?? ?if (item.type === 'Identifier') {

          ? ?? ?? ?? ?? ?? ?? ?? ?? ? if (item.name === funcArgs0 index === 1) {
          ? ?? ?? ?? ?? ?? ?? ?? ?? ?? ???isSwap = true
          ? ?? ?? ?? ?? ?? ?? ?? ?? ? } else if (item.name === funcArgs1 index === 0) {
          ? ?? ?? ?? ?? ?? ?? ?? ?? ?? ???isSwap = true
          ? ?? ?? ?? ?? ?? ?? ?? ?? ? }
          ? ?? ?? ?? ?? ?? ?? ?? ?? ? break;
          ? ?? ?? ?? ?? ?? ?? ?? ?}
          ? ?? ?? ?? ?? ?? ???}
          ? ?? ?? ?? ?? ?? ???const handleExpression = (bodyExpress, argsIdentifier) => {
          ? ?? ?? ?? ?? ?? ?? ?? ?if (bodyExpress.type !== 'BinaryExpression') {
          ? ?? ?? ?? ?? ?? ?? ?? ?? ? return argsIdentifier
          ? ?? ?? ?? ?? ?? ?? ?? ?}
          ? ?? ?? ?? ?? ?? ?? ?? ?const handleIdentifier = (item) => {
          ? ?? ?? ?? ?? ?? ?? ?? ?? ? if (item.type !== 'Identifier') {
          ? ?? ?? ?? ?? ?? ?? ?? ?? ?? ???return item
          ? ?? ?? ?? ?? ?? ?? ?? ?? ? } else {
          ? ?? ?? ?? ?? ?? ?? ?? ?? ?? ???return argsIdentifier
          ? ?? ?? ?? ?? ?? ?? ?? ?? ? }
          ? ?? ?? ?? ?? ?? ?? ?? ?}
          ? ?? ?? ?? ?? ?? ?? ?? ?const numAst = types.binaryExpression(bodyExpress.operator, handleIdentifier(bodyExpress.left), handleIdentifier(bodyExpress.right))
          ? ?? ?? ?? ?? ?? ?? ?? ?const numResult = eval(generator(numAst).code)
          ? ?? ?? ?? ?? ?? ?? ?? ?return types.numericLiteral(numResult)
          ? ?? ?? ?? ?? ?? ???}
          ? ?? ?? ?? ?? ?? ???const firstIdentifier = path.node.arguments[0]
          ? ?? ?? ?? ?? ?? ???const secondIdentifier = path.node.arguments[1]
          ? ?? ?? ?? ?? ?? ???let newCalleeArgs = [handleExpression(bodyCallArgs[0], isSwap ? secondIdentifier : firstIdentifier), handleExpression(bodyCallArgs[1], isSwap ? firstIdentifier : secondIdentifier)]
          ? ?? ?? ?? ?? ?? ???let newNode = types.callExpression(funcNode.body.body[0].argument.callee, newCalleeArgs);
          ? ?? ?? ?? ?? ?? ???path.replaceInline(newNode)
          ? ?? ?? ?? ?? ? }
          ? ?? ?? ?? ?}
          ? ?? ???},
          ? ? });

          然后獲取解密的函數,這里因為比較偷懶,所以直接使用了正則表達式計算關鍵函數

           復制代碼 隱藏代碼
          function generatorHandleCrackStringFunc(text) {
          ? ? const matchResult = text.match(/\d{4,}\);\s?(function.*),\s?[A-Za-z].[A-Za-z]\s?=\s?[A-Za-z]/)
          ? ? if (matchResult.length !== 2) {
          ? ?? ???throw new Error('代碼解析失敗!')
          ? ? }
          ? ? const funcName = matchResult[1].match(/function ([A-Za-z])\([A-Za-z],\s?[A-Za-z]\).*(?=abc)/)[1]
          ? ? return {
          ? ?? ???crackName: funcName,
          ? ?? ???crackCharFunc: new Function([], matchResult[1] + ';return function(num,char){return ' + funcName + '(num, char)}')()
          ? ? }
          }

          然后調用解密函數

           復制代碼 隱藏代碼
          ? ? traverse(ast, {
          ? ?? ???CallExpression(path) {
          ? ?? ?? ?? ?if (path.node.arguments.length === 2) {
          ? ?? ?? ?? ?? ? if (path.node.callee.name !== name) {
          ? ?? ?? ?? ?? ?? ???return
          ? ?? ?? ?? ?? ? }
          ? ?? ?? ?? ?? ? if (path.node.arguments[0].type !== 'NumericLiteral') {
          ? ?? ?? ?? ?? ?? ???return;
          ? ?? ?? ?? ?? ? }
          ? ?? ?? ?? ?? ? if (path.node.arguments[1].type !== 'StringLiteral') {
          ? ?? ?? ?? ?? ?? ???return;
          ? ?? ?? ?? ?? ? }
          ? ?? ?? ?? ?? ? const nodeResult = handleStringFunc(path.node.arguments[0].value, path.node.arguments[1].value)
          ? ?? ?? ?? ?? ? path.replaceInline(types.stringLiteral(nodeResult))
          ? ?? ?? ?? ?}
          ? ?? ???},
          ? ? });

          然后對解密后的字符串和數字等做一下合并

           復制代碼 隱藏代碼
          ? ? const handleObfs = {
          ? ?? ???CallExpression: {
          ? ?? ?? ?? ?exit(outerPath) {
          ? ?? ?? ?? ?? ? const node = outerPath.node.callee
          ? ?? ?? ?? ?? ? const parentPath = outerPath
          ? ?? ?? ?? ?? ? if (node?.object?.type === 'Identifier' node?.property?.type === 'StringLiteral') {
          ? ?? ?? ?? ?? ?? ???const objBinding = outerPath.scope.getBinding(node.object.name)
          ? ?? ?? ?? ?? ?? ???if (objBinding === undefined) {
          ? ?? ?? ?? ?? ?? ?? ?? ?return;
          ? ?? ?? ?? ?? ?? ???}
          ? ?? ?? ?? ?? ?? ???const objNode = objBinding.path.node
          ? ?? ?? ?? ?? ?? ???const funcList = objNode.init?.properties ?? []
          ? ?? ?? ?? ?? ?? ???const funcInstance = funcList.find((item) => {
          ? ?? ?? ?? ?? ?? ?? ?? ?const keyName = item.key.name
          ? ?? ?? ?? ?? ?? ?? ?? ?return keyName === node.property.value
          ? ?? ?? ?? ?? ?? ???})
          ? ?? ?? ?? ?? ?? ???if (funcInstance) {
          ? ?? ?? ?? ?? ?? ?? ?? ?const parentNode = parentPath.node

          ? ?? ?? ?? ?? ?? ?? ?? ?let replaceAst = null
          ? ?? ?? ?? ?? ?? ?? ?? ?if (funcInstance.value.type === 'FunctionExpression') {
          ? ?? ?? ?? ?? ?? ?? ?? ?? ? const originNode = funcInstance.value.body.body[0].argument
          ? ?? ?? ?? ?? ?? ?? ?? ?? ? //函數
          ? ?? ?? ?? ?? ?? ?? ?? ?? ? if (originNode.type === 'CallExpression') {
          ? ?? ?? ?? ?? ?? ?? ?? ?? ?? ???replaceAst = types.callExpression(parentNode.arguments[0], [...parentNode.arguments].splice(1))
          ? ?? ?? ?? ?? ?? ?? ?? ?? ? } else if (originNode.type === 'BinaryExpression') {
          ? ?? ?? ?? ?? ?? ?? ?? ?? ?? ???replaceAst = types.binaryExpression(originNode.operator, parentNode.arguments[0], parentNode.arguments[1])
          ? ?? ?? ?? ?? ?? ?? ?? ?? ? }
          ? ?? ?? ?? ?? ?? ?? ?? ?} else {
          ? ?? ?? ?? ?? ?? ?? ?? ?? ? //字符串
          ? ?? ?? ?? ?? ?? ?? ?? ?? ? debugger
          ? ?? ?? ?? ?? ?? ?? ?? ?? ? replaceAst = types.stringLiteral(funcInstance.value.value)
          ? ?? ?? ?? ?? ?? ?? ?? ?}
          ? ?? ?? ?? ?? ?? ?? ?? ?if (replaceAst) {
          ? ?? ?? ?? ?? ?? ?? ?? ?? ? parentPath.replaceWith(replaceAst)

          ? ?? ?? ?? ?? ?? ?? ?? ?}

          ? ?? ?? ?? ?? ?? ???}
          ? ?? ?? ?? ?? ? }
          ? ?? ?? ?? ?}
          ? ?? ???},
          ? ?? ???MemberExpression: {
          ? ?? ?? ?? ?enter(path) {
          ? ?? ?? ?? ?? ? const node = path.node
          ? ?? ?? ?? ?? ? if (node?.object?.type === 'Identifier' node?.property?.type === 'StringLiteral') {
          ? ?? ?? ?? ?? ?? ???const objBinding = path.scope.getBinding(node.object.name)
          ? ?? ?? ?? ?? ?? ???if (objBinding === undefined) {
          ? ?? ?? ?? ?? ?? ?? ?? ?return;
          ? ?? ?? ?? ?? ?? ???}
          ? ?? ?? ?? ?? ?? ???const objNode = objBinding.path.node
          ? ?? ?? ?? ?? ?? ???const funcList = objNode.init?.properties ?? []
          ? ?? ?? ?? ?? ?? ???const funcInstance = funcList.find((item) => {
          ? ?? ?? ?? ?? ?? ?? ?? ?const keyName = item.key.name
          ? ?? ?? ?? ?? ?? ?? ?? ?return keyName === node.property.value
          ? ?? ?? ?? ?? ?? ???})
          ? ?? ?? ?? ?? ?? ???if (funcInstance) {
          ? ?? ?? ?? ?? ?? ?? ?? ?let replaceAst = null
          ? ?? ?? ?? ?? ?? ?? ?? ?if (funcInstance.value.type === 'StringLiteral') {
          ? ?? ?? ?? ?? ?? ?? ?? ?? ? replaceAst = types.stringLiteral(funcInstance.value.value)
          ? ?? ?? ?? ?? ?? ?? ?? ?}
          ? ?? ?? ?? ?? ?? ?? ?? ?if (replaceAst) {
          ? ?? ?? ?? ?? ?? ?? ?? ?? ? path.replaceWith(replaceAst)
          ? ?? ?? ?? ?? ?? ?? ?? ?}

          ? ?? ?? ?? ?? ?? ???}
          ? ?? ?? ?? ?? ? }
          ? ?? ?? ?? ?}
          ? ?? ???}
          ? ? }

          ? ? traverse(ast, handleObfs);

          我們可以從已經解密的文件里提取一些關鍵字符串

           復制代碼 隱藏代碼
          ? ? const mathRsult = code.match(/\[\"(.*)\", [a-zA-Z]\[\"time\"\][\s\S]*\[\"sign\"\] = \[\"([0-9]*)\".*function \(([a-zA-Z])\) {([\s\S]*)}\([a-zA-Z]\)\,.*?"([a-zA-Z0-9]{3,})"/)
          ? ? if (mathRsult.length !== 6) {
          ? ?? ???throw new Error('密鑰解析失敗!')
          ? ? }
          ? ? const signPrefix = mathRsult[2]
          ? ? const signEnd = mathRsult[5]
          ? ? const prefixToken = mathRsult[1]
          ? ? const hashFunc = new Function(mathRsult[3], mathRsult[4])

          接下來直接調試可以解出來BCToken的算法

           復制代碼 隱藏代碼
          ? ? function generateBcToken() {
          ? ?? ???if (bcToken !== "") {
          ? ?? ?? ?? ?return bcToken
          ? ?? ???}
          ? ?? ???const V = () => 1e12 * Math.random()
          ? ?? ???const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36'
          ? ?? ???const hash = sha1.create();
          ? ?? ???const text = [(new Date).getTime(), V(), V(), UA].map(btoa).join(".")
          ? ?? ???console.log(text)
          ? ?? ???hash.update(text);
          ? ?? ???bcToken = hash.hex()
          ? ?? ???return bcToken
          ? ? }

          Sign加密算法也可以解出來了

           復制代碼 隱藏代碼
          ? ? function generateSha({ url, auth_id }) {
          ? ?? ???const fixPrefix = prefixToken;
          ? ?? ???let time = +new Date();
          ? ?? ???const toeknURL = [fixPrefix, time, url, auth_id || 0].join(`\n`);
          ? ?? ???const hash = sha1.create();
          ? ?? ???hash.update(toeknURL);
          ? ?? ???return {
          ? ?? ?? ?? ?token: hash.hex(),
          ? ?? ?? ?? ?time: time
          ? ?? ???}
          ? ? }
          ? ?? ? function??getSign({ url, auth_id }) {
          ? ?? ?? ?? ?const { time, token } = generateSha({ url, auth_id })
          ? ?? ?? ?? ?return {
          ? ?? ?? ?? ?? ? sign: [signPrefix, token, hashFunc(token), signEnd].join(':'),
          ? ?? ?? ?? ?? ? time: time
          ? ?? ?? ?? ?}
          ? ?? ???}

          那基本的算法解密就搞定了,但是最近還更新了DRM


          其中給了一個mpt和m3u8
          分別有不同的密鑰
          根據測試DRM的密鑰是需要寫在Cookies里的
          但是詭異的事情來了
          postman可以測試成功,cmd測試失敗,代碼測試失敗,powershell測試成功
          ffmpeg測試也失敗

          我的第一反應可能是TLS指紋校驗了
          這部分事后發現1.1也可以了,只要同ip就行,我也不確定到底是我測試錯誤還是后期改了
          所以這部分可以直接忽略,但是因為我自己覺得補上HTTP2的代碼有利于思路的連貫性分析和大家下次直接抄輪子
          思慮之后決定保留了下來
          于是在https://github.com/nodejs/undici/issues/1983
          抄了一段,改成OF網站的,這里就按下不表了

           復制代碼 隱藏代碼
          const undici = require("undici")
          const tls = require("tls")

          // From https://httptoolkit.com/blog/tls-fingerprinting-node-js/
          const defaultCiphers = tls.DEFAULT_CIPHERS.split(':');
          const shuffledCiphers = [
          ? ? defaultCiphers[1],
          ? ? defaultCiphers[2],
          ? ? defaultCiphers[0],
          ? ? ...defaultCiphers.slice(3)
          ].join(':');

          const connector = undici.buildConnector({ ciphers: shuffledCiphers })
          const client = new undici.Client("https://en.zalando.de", { connect: connector })

          undici.request("https://en.zalando.de/api/navigation", {
          ? ? dispatcher: client,
          ? ? headers: {
          ? ?? ???"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
          ? ? }
          }).then(async (res) => {
          ? ? const body = await res.body.json()
          ? ? console.log(body)
          })

          依然沒有成功,這個時候還跟無頭蒼蠅一樣打轉,我認為可能是TLS因為Node修改的不徹底導致的,決定切換Go技術棧試試
          于是找到了https://juejin.cn/post/7073264626506399751#heading-4
          測試驚覺發現竟然是HTTTP2
          于是返回抓包看了一眼
          發現確實都是HTTP2!


          那果斷切一下HTTP2的通信協議試一下

           復制代碼 隱藏代碼
          js
          const http2 = require("http2");
          const client = http2.connect("https://cdn3.OF網站.com");

          const req = client.request({
          ??":method": "GET",
          ??":path": "/dash/files/3/3f/XXX/XXX.mpd",
          ??"accept": "*/*",
          ??"accept-language": "zh-CN,zh;q=0.9",
          ??"cache-control": "no-cache",
          ??"pragma": "no-cache",
          ??"priority": "u=1, i",
          ??"sec-ch-ua": "\"Not/A)Brand\";v=\"8\", \"Chromium\";v=\"126\", \"Google Chrome\";v=\"126\"",
          ??"sec-ch-ua-mobile": "?0",
          ??"sec-ch-ua-platform": "\"Windows\"",
          ??"sec-fetch-dest": "empty",
          ??"sec-fetch-mode": "cors",
          ??"sec-fetch-site": "same-site",
          ??"cookie": "保護隱私",
          ??"Referer": "https://OF網站.com/",
          ??"Referrer-Policy": "strict-origin-when-cross-origin"
          });

          let data = "";

          req.on("response", (headers, flags) => {
          ??for (const name in headers) {
          ? ? console.log(`${name}: ${headers[name]}`);
          ??}

          });

          req.on("data", chunk => {
          ??data += chunk;
          });
          req.on("end", () => {
          ??console.log(data);
          ??client.close();
          });
          req.end();

          果然成功讀取到數據!


          根據查看同類庫OF-DRM (這個庫真的幫助了我很多思路)
          可以發現使用了一個yt-dlp
          我們可以找一個nodejs版本的
          測試代碼如下

           復制代碼 隱藏代碼
          const path = require('path');

          const YTDlpWrap = require('yt-dlp-wrap').default;

          const ytDlpWrap = new YTDlpWrap(path.join('./yt-dlp_x86.exe'));
          let ytDlpEventEmitter = ytDlpWrap
          ? ? .exec([
          ? ?? ???'https://cdn3.OF網站.com/hls/files/a/a2/xxx/xxx.m3u8',
          ? ?? ???"-f",
          ? ?? ???"bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best[ext=m4a]",
          ? ?? ???"--allow-u",
          ? ?? ???"--no-part",
          ? ?? ???"--restrict-filenames",
          ? ?? ???"-N 4",
          ? ?? ???'--add-headers',
          ? ?? ???`Cookie:"個人隱私"`,
          ? ?? ???'-o',
          ? ?? ???'F:/vmware/output2.mp4',
          ? ? ])
          ? ? .on('progress', (progress) =>
          ? ?? ???console.log(
          ? ?? ?? ?? ?progress.percent,
          ? ?? ?? ?? ?progress.totalSize,
          ? ?? ?? ?? ?progress.currentSpeed,
          ? ?? ?? ?? ?progress.eta
          ? ?? ???)
          ? ? )
          ? ? .on('ytDlpEvent', (eventType, eventData) =>
          ? ?? ???console.log(eventType, eventData)
          ? ? )
          ? ? .on('error', (error) => console.error(error))
          ? ? .on('close', () => console.log('all done'));

          console.log(ytDlpEventEmitter.ytDlpProcess.pid);

          我也是剛接觸,不一定參數描述的正確,-f表示格式,allow-u表示允許無法格式化的視頻下載,no-part不要使用分割部分文件,restrict-filenames貌似是控制短標題和特殊字符的,-N應該是多線程
          只有使用這套能繞過DRM的版權下載問題

          下載完成后發現依然沒法播放
          根據研究是視頻使用了加密
          這個時候可以根據技術棧下手
          根據搜索drm找到了 "DRM encrypted source cannot be decrypted without a DRM plugin
          根據上下文找到videojs字樣


          所以懷疑是videojs
          于是找videojs的DRM庫,找到了
          https://github.com/videojs/videojs-contrib-eme?tab=readme-ov-file#using
          使用例子是

           復制代碼 隱藏代碼
          player.eme();
          player.src({
          ??src: '<your url here>',
          ??type: 'application/dash+xml',
          ??keySystems: {
          ? ? 'com.widevine.alpha': '<YOUR URL HERE>'
          ??}
          });

          在網頁中搜索eme,發現也能找到,下一個斷點之后調試打印src的o內容


          根據文檔getLicense()- 允許異步檢索許可證。
          所以我們目前應該主攻getLicense()函數了
          其中代碼為

           復制代碼 隱藏代碼
          ? ?? ?? ?? ?? ?? ???getLicense: (e,s,o)=>{
          ? ?? ?? ?? ?? ?? ?? ?? ?j.vM.xhr({
          ? ?? ?? ?? ?? ?? ?? ?? ?? ? url: i,
          ? ?? ?? ?? ?? ?? ?? ?? ?? ? method: "POST",
          ? ?? ?? ?? ?? ?? ?? ?? ?? ? responseType: "arraybuffer",
          ? ?? ?? ?? ?? ?? ?? ?? ?? ? body: new Uint8Array(s),
          ? ?? ?? ?? ?? ?? ?? ?? ?? ? headers: {
          ? ?? ?? ?? ?? ?? ?? ?? ?? ?? ???"Content-type": "application/octet-stream",
          ? ?? ?? ?? ?? ?? ?? ?? ?? ?? ???...t
          ? ?? ?? ?? ?? ?? ?? ?? ?? ? },
          ? ?? ?? ?? ?? ?? ?? ?? ?? ? withCredentials: !0
          ? ?? ?? ?? ?? ?? ?? ?? ?}, ((e,i,t)=>{
          ? ?? ?? ?? ?? ?? ?? ?? ?? ? e ? o(e) : o(null, t)
          ? ?? ?? ?? ?? ?? ?? ?? ?}
          ? ?? ?? ?? ?? ?? ?? ?? ?))
          ? ?? ?? ?? ?? ?? ???}

          往上一層看
          這里可以看到創建了一個promise,當調用獲取許可時會回調y,而y會把數據觸發調用promise的Resolve出去,導致普通人很容易跟丟


          實際上接下來的流程處理在

          其中u是MediaKeySession,讀取了我們的密鑰,而MediaKeySession的接口代表與內容解密模塊 (CDM) 進行消息交換的上下文。


          以CDM為關鍵詞,可以搜到https://www.freebuf.com/articles/database/375523.html

           復制代碼 隱藏代碼
          全球現有三大實現方案,分別為谷歌的Widevine、蘋果的FairPlay和微軟的PlayReady。其中Widevine實現簡單,免費,市場占有率最高,應用最廣泛。Widevine客戶端主要內置于手機、電視、各大瀏覽器、播放器等,用于解密被保護的視頻。

          Widevine擁有三個安全級別——L1、L2和L3。L1是最高的安全級別,解密全過程在硬件中完成,需要設備支持。L3的安全級別最低,解密全程在CDM(Content Decryption Module )軟件中完成。L2介于兩者之間, 核心解密過程在硬件完成,視頻處理階段在軟件中完成。本文只討論L3級視頻的解密方式。

          既然我們是谷歌瀏覽器,那我們大概率是Widevine的DRM保護了
          那接下來的目標就是如何解密CDM
          既然已經確定了是wvd l3
          我們需要獲取解密mp4的密鑰
          需要ppsh和License URL
          找到的wvd代碼來自https://forum.videohelp.com/threads/414040-Need-some-help-to-download-drm-protected-video-from-this-free-service
          這里我截取片段

           復制代碼 隱藏代碼
          WVD_FILE = "device_wvd_file.wvd"

          PLAYER_URL = 'https://aloula.faulio.com/api/v1/video/{video_id}/player'
          ORIGIN = "https://www.aloula.sa"

          def get_keys(pssh_value, license_url):
          ? ? if pssh_value is None:
          ? ?? ???return []
          ? ? try:
          ? ?? ???device = Device.load(WVD_FILE)
          ? ? except:
          ? ?? ???return []

          ? ? pssh_value = PSSH(pssh_value)
          ? ? cdm = Cdm.from_device(device)
          ? ? cdm_session_id = cdm.open()

          ? ? challenge = cdm.get_license_challenge(cdm_session_id, pssh_value)
          ? ? licence = requests.post(
          ? ?? ???license_url, data=challenge,
          ? ?? ???headers={"Origin": ORIGIN}
          ? ? )
          ? ? licence.raise_for_status()
          ? ? cdm.parse_license(cdm_session_id, licence.content)

          ? ? keys = []
          ? ? for key in cdm.get_keys(cdm_session_id):
          ? ?? ???if "CONTENT" in key.type:
          ? ?? ?? ?? ?keys += [f"{key.kid.hex}:{key.key.hex()}"]
          ? ? cdm.close(cdm_session_id)
          ? ? return keys

          ppsh和licence屬于網站提取的內容,那wvd是什么?
          Create a Widevine Device (.wvd) file from an RSA Private Key (PEM or DER) and Client ID Blob.
          wvd是Widevine Device ,是根據一個RSA私鑰和Client IDBlob生成的
          其提取的方法我在
          https://forum.videohelp.com/threads/404994-Decryption-and-the-Temple-of-Doom
          找到了,當然也可以使用現有的,但是本著蘇格拉底式學習的思想,決定嘗試手動提取WVD
          另外也找到了一個疑似可以在線處理的網站
          https://cdrm-project.com/
          同時這個網站也提供了大量的WVD DRM分析的文章和工具
          https://cdm-project.com/

          安卓root提取WVD

          注意!!!根據測試模擬器沒有WVD,不要嘗試在模擬器搞
          首先需要root和安裝magisk
          然后在magisk的設置的超級用戶訪問選擇用戶和ADB,重啟



          然后安裝MagiskFrida
          https://github.com/ViRb3/magisk-frida/releases
          下載出來在magisk導入模塊
          最好也裝上L1回退模塊
          https://github.com/hzy132/liboemcryptodisabler/releases/tag/v1.5.1
          全部搞定之后安裝adb,為了圖方便可以直接把adb的目錄塞到path里
          這樣就有adb命令了
          輸入adb查看有沒有手機
          確定有之后拉取https://github.com/hyugogirubato/KeyDive的代碼
          輸入 pip install -r requirements.txt 安裝依賴
          因為adb devices找到了


           復制代碼 隱藏代碼
          List of devices attached
          emulator-5554? ?device

          輸入 python keydive.py -a -d ‘emulator-5554’ -w 即可導出

          就是這樣,你現在應該有一個以 ClientId 和 Private_key.pem 形式存在的 CDM,它們藏在 Keydive 文件夾根目錄中的設備中(因為我本機沒root,模擬器又復現失敗了...所以這步要靠自己了,不過應該大差不差,因為我AVD提取成功了~)

          AVD提取WVD

          因為模擬器不支持wvd DRM
          所以根據https://forum.videohelp.com/threads/408031-Dumping-Your-own-L3-CDM-with-Android-Studio
          嘗試andirod Studio獲取DRM


          安裝pixel 6 (系統一定要選Pie,不然frida-server會不成功)


          然后啟動

          啟動成功后在Window安裝腳本 pip install frida pip install frida-tools
          接下來輸入 pip list 查看包版本

          然后下載對應版本的frida-server
          https://github.com/frida/frida/releases
          我的是16.2.5則去下 frida-server-16.2.5-android-x86.xz 然后解壓得到frida-server-16.2.5-android-x86
          然后輸入
          adb push C:\Users\lihengdao\Downloads\frida-server-16.2.5-android-x86 /sdcard
          移動之后輸入

           復制代碼 隱藏代碼
          adb.exe shell
          su
          mv /sdcard/frida-server-16.2.5-android-x86 /data/local/tmp
          chmod +x /data/local/tmp/frida-server-16.2.5-android-x86
          /data/local/tmp/frida-server-16.2.5-android-x86

          運行有點報錯很正常,直接繼續
          拉取項目 https://github.com/wvdumper/dumper
          安裝依賴 pip3 install -r requirements.txt
          然后降級一下protobuf pip install protobuf==3.20.*
          輸入 python .\dump_keys.py 運行,注意運行frida-server的窗口不要關
          顯示Hook completed就成功了


          接下來在Andriod Studio的Pixel模擬器訪問https://bitmovin.com/demos/drm
          小提示,這里建議設置代{過}{濾}理,模擬器的回環代{過}{濾}理是10.0.2.2
          將wifi的設置里proxy設置上相應的回環地址和端口即可
          如果網絡不好加載不出來視頻會存在bin和pem文件的!
          https://developer.android.com/studio/run/emulator-networking?hl=zh-cn
          視頻沒刷出來就多試試
          大陸網有點卡
          當出現視頻進度點播放
          就會在dumper-main目錄里生成劫持到的文件

          然后去生成的文件目錄輸入 pywidevine create-device -k private_key.pem -c client_id.bin -t "CHROME" -l 3 -o wvd
          wvd驅動文件生成成功!

          -官方論壇

          www.52pojie.cn



          主站蜘蛛池模板: 日韩一区二区a片免费观看| 国产成人av一区二区三区在线观看 | 精品国产一区二区三区久久狼| 一区二区三区在线播放视频| 无码人妻久久一区二区三区| 国产色精品vr一区区三区| 国产精品无码一区二区三区毛片| 国产精品女同一区二区| 精品伦精品一区二区三区视频 | 无码人妻精品一区二区三区99性| 国产亚洲综合一区柠檬导航| 精品国产不卡一区二区三区| 国产aⅴ一区二区| 精品国产亚洲第一区二区三区| 亚洲AV成人一区二区三区观看 | 综合人妻久久一区二区精品| 香蕉视频一区二区三区| 久久精品国产一区二区三区不卡| 日本精品一区二区三区四区| 国产在线观看精品一区二区三区91| 一本一道波多野结衣一区| 成人精品一区二区户外勾搭野战 | 国产福利一区二区在线视频 | 久久久久人妻精品一区| 免费播放一区二区三区| 亚洲福利视频一区二区三区 | 久久精品综合一区二区三区| 国产精品第一区第27页| 日本亚洲国产一区二区三区| 亚洲熟女少妇一区二区| 一区二区三区观看免费中文视频在线播放 | 欧洲精品无码一区二区三区在线播放| 精品一区二区三区水蜜桃| 一区二区三区免费在线视频| 久久影院亚洲一区| 秋霞午夜一区二区| 亚洲一区在线免费观看| 大帝AV在线一区二区三区| 一区二区三区免费视频网站| 国产品无码一区二区三区在线蜜桃| 亚洲国产精品一区二区久久hs|