當前位置: 妍妍網 > 碼農

前端 JS 安全對抗原理與實踐

2024-03-23碼農

架構師(JiaGouX)

我們都是架構師!
架構未來,你來不來?

前端程式碼都是公開的,為了提高程式碼的破解成本、保證JS程式碼裏的一些重要邏輯不被居心叵測的人利用,需要使用一些加密和混淆的防護手段。


一、概念解析


1.1 什麽是介面加密


如今這個時代,數據已經變得越來越重要,網頁和APP是主流的數據載體,如果獲取數據的介面沒有設定任何的保護措施的話,數據就會被輕易地竊取或篡改。

除了數據泄露外,一些重要功能的介面如果沒有做好保護措施也會被惡意呼叫造成DDoS、條件競爭等攻擊效果,比如如下幾個場景:

一些行銷活動類的Web頁面,領紅包、領券、投票、抽獎等活動方式很常見。此類活動對於普通使用者來說應該是「拼手氣」,而對於非正常使用者來說,可以透過直接刷活動API介面的這種「作弊」方式來提升「手氣」。這樣對普通使用者來說就很不公平。

所以對重要介面都會采用加密驗簽的方式進行保護,而驗簽的加密邏輯大多數都透過JS程式碼實作,所以保護JS程式碼不被攻擊者竊取尤為重要。



1.2 為什麽要保護JS程式碼

  • JavaScript程式碼執行於客戶端

  • JavaScript程式碼是公開透明的

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


    1.3 套用場景


    以下場景就透過特定的防護措施提高了攻擊成本:

  • 某些網站會在頁面中使用JavaScript對數據進行加密,以保護數據的安全性和私密性,在爬取時需要透過解密JavaScript程式碼才能獲取到數據。

  • 某些網站的URL會有某個參數帶有一些看不太懂的長串加密參數,攻擊者要爬取的話就必須要知道這些參數是怎麽構造的,否則無法正確地存取該URL。

  • 翻看網站的JavaScript原始碼,可以發現很多壓縮了或者看不太懂的字元,比如JavaScript檔名被編碼,JavaScript的檔內容都壓縮成幾行,JavaScript變量也被修改成單個字元或者一些十六進制的字元,所以我們不能輕易地根據JavaScript找出某些介面的加密邏輯。


  • 1.4 涉及的技術


    這些場景都是網站為了保護數據不被輕易抓取采取的措施,運用的技術主要有:

  • 介面加密技術

  • JavaScript壓縮、混淆和加密技術


  • 二、技術原理


    2.1 介面加密技術


    數據和功能一般是透過伺服器提供的介面來實作,為了提升介面的安全性,客戶端會和伺服端約定一種介面檢驗方式,通常是各種加密和編碼演算法,如Base64、Hex、MD5、AES、DES、RSA等。

    常用的數據介面都會攜帶一個sign參數用於許可權管控:

    ① 客戶端和伺服端約定一種介面校驗邏輯,客戶端在每次請求伺服端介面的時候附帶一個sign參數。
    ② sign參數的邏輯自訂,可以由當前時間戳資訊、裝置ID、日期、雙方約定好的秘鑰經過一些加密演算法構造而成。
    ③ 客戶端根據約定的加密演算法構造sign,每次請求伺服器的時候附帶上sign數。
    ④ 伺服端根據約定的加密演算法和請求的數據對sign進行校驗,如果檢驗透過,才返回數據,否則拒絕響應。

    這就是一個比較簡單的介面參數加密的實作,如果有人想要呼叫這個介面的話,必須要破解sign的生成邏輯,否則是無法正常呼叫介面的。

    當然上面的實作思路比較簡單,還可以增加一些時間戳資訊和存取頻次來增延長效性判斷,或使用非對稱加密提高加密的復雜程度。

    實作介面參數加密需要用到一些加密演算法,客戶端和伺服器都有對應的SDK來實作這些加密演算法,如JavaScript的crypto-js、Python的hashlib、Crypto等等。如果是網頁且客戶端的加密邏輯是用JavaScript來實作的話,其原始碼對使用者是完全可見的,所以我們需要用壓縮、混淆、加密的方式來對JavaScript程式碼進行一定程度的保護。


    2.2 什麽是壓縮


    去除JavaScript程式碼中不必要的空格、換行等內容,使源碼都壓縮為幾行內容,降低程式碼可讀性,同時可提高網站的載入速度。

    如果僅僅是去除空格換行這樣的壓縮方式,幾乎沒有任何防護作用,這種壓縮方式僅僅是降低了程式碼的直接可讀性,可以用IDE、線上工具或Chrome輕松將JavaScript程式碼變得易讀。

    所以JavaScript壓縮技術只能在很小的程度上起到防護作用,想提高防護的效果還得依靠JavaScript混淆和加密技術。


    2.3 什麽是混淆


    使用變量混淆、字串混淆、內容加密、控制流平坦化、偵錯保護、多型變異等手段,使程式碼變得難以閱讀和分析,同時不影響程式碼原有功能,是一種理想且實用的JS保護方案。

  • 變量混淆 :將變量名、方法名、常量名隨機變為無意義的亂碼字串,降低程式碼可讀性,如轉成單個字元或十六進制字串。

  • 字串混淆 :將字串陣列化集中放置,並進行MD5或Base64編碼儲存,使程式碼中不出現明文字串,可以避免使用全域搜尋字串的方式定位到入口點。

  • 內容加密 :針對JavaScript物件的內容進行加密轉化,隱藏程式碼之間的呼叫關系,把key-value的對映關系混淆掉。

  • 控制流平坦化 :打亂函式原有程式碼執行流程及函式呼叫關系,使程式碼邏輯變得混亂無序。

  • 偵錯保護 :基於偵錯程式特性,加入一些強制偵錯debug語句,無限debug、定時debug、debug關鍵字,使其在偵錯模式下難以順利執行JavaScript程式碼。

  • 多型變異 :JavaScript程式碼每次被呼叫時,程式碼自身立刻自動發生變異,變化為與之前完全不同的程式碼,避免程式碼被動態分析偵錯。

  • 2.4 什麽是加密

    JavaScript加密是對JavaScript混淆技術防護的進一步升級,基本思路是將一些核心邏輯用C/C++語言來編寫,並透過JavaScript呼叫執行,從而起到二進制級別的防護作用,加密的方式主要有Emscripten和WebAssembly等。

    1. Emscripten

    Emscripten編譯器可以將C/C++程式碼編譯成asm.js的JavaScript變體,再由JavaScript呼叫執行,因此某些JavaScript的核心功能可以使用C/C++語言實作。

    2.WebAssembly

    WebAssembly也能將C/C++程式碼轉成JavaScript引擎可以執行的程式碼,但轉出來的程式碼是二進制字節碼,而asm.js是文本,因此執行速度更快、體積更小,得到的字節碼具有和JavaScript相同的功能,在語法上完全脫離JavaScript,同時具有沙盒化的執行環境,利用WebAssembly技術,可以將一些核心的功能用C/C++語言實作,形成瀏覽器字節碼的形式,然後在JavaScript中透過類似如下的方式呼叫:

    這種加密方式更加安全,想要逆向或破解需要逆向WebAssembly,難度極大。


    2.5 工具介紹


    2.5.1 壓縮混淆工具

  • Uglifyjs (開源):

    用NodeJS編寫的JavaScript壓縮工具,是目前最流行的JS壓縮工具,JQuery就是使用此工具壓縮,UglifyJS壓縮率高,壓縮選項多,並且具有最佳化程式碼,格式化程式碼功能。

  • jshaman

    jshaman是一個商業級工具,看了很多社群的評論,這個目前是最好的,可以線上免費使用,也可以購買商業版。

  • jsfuck

    開源的js混淆工具,原理比較簡單,透過特定的字串加上下標定位字元,再由這些字元替換原始碼,從而實作混淆。

  • YUI Compressor:

    業界巨頭yahoo提供的一個前端壓縮工具,透過java庫編譯css或js檔進行壓縮


  • 2.5.2 反混淆工具

  • jsbeautifier

    jsbeautifier是一個為前端開發人員制作的Chrome擴充套件,能夠直接檢視經過壓縮的Javascript程式碼。

  • UnuglifyJS

    壓縮工具uglify對應的解混淆工具。

  • jspacker

    用PHP編寫的壓縮工具,可以混淆程式碼保護智慧財產權,產生的程式碼相容IE、FireFox等常用瀏覽器,國內大部份線上工具網站都采用這種演算法壓縮。


  • 三、前端安全對抗


    3.1 前端偵錯手法


    3.1.1 Elements

    Elements 面板會顯示目前網頁中的 DOM、CSS 狀態,且可以修改頁面上的 DOM 和 CSS,即時看到結果,省去了在編輯器修改、儲存、瀏覽器檢視結果的流程。

    有時候一些dom節點會巢狀很深,導致我們很難利用Element面板html程式碼來找到對應的節點。inspect(dom元素)可以讓我們快速跳轉到對應的dom節點的html程式碼上。

    3.1.2 Console

    Console物件提供了瀏覽器控制台偵錯的介面,Console是一個物件,上面有很多方便的方法。

  • console.log( ) :最常用的語句,可以將變量輸出到瀏覽器的控制台中,方便開發者呼叫JS程式碼

  • console.table( ) :可用於打印obj/arr成表格

  • console.trace( ) :可用於debugger堆疊偵錯,方便檢視程式碼的執行邏輯,看一些庫的源碼

  • console.count( ) :打印標簽被執行了幾次,預設值是default,可用在快速計數

  • console.countReset( ) :用來重設,可用在計算單次行為的觸發的計數

  • console.group( )/console.groupEnd( )

    為了方便一眼看到自己的log,可以用console.group自訂message group標簽,還可以多層巢狀,並用console.groupEnd來關閉Group。

  • 3.1.3 JS斷點偵錯

    JS斷點偵錯,即在瀏覽器開發者工具中為JS程式碼添加斷點,讓JS執行到某一特定位置停住,方便開發者對該處程式碼段進行分析與邏輯處理。

    Sources面板

    ① 普通斷點(breakpoint)

    給一段程式碼添加斷點的流程是:"F12(Ctrl + Shift + I)開啟開發工具"->"點選Sources選單"->"左側樹中找到相應檔"→"點選行號列"即完成在當前行添加/刪除斷點操作。當斷點添加完畢後,重新整理頁面JS執行到斷點位置停住,在Sources界面會看到當前作用域中所有變量和值。

  • 恢復(Resume) : 恢復按鈕(第一個按鈕),繼續執行,快捷鍵 F8,繼續執行,如果沒有其他的斷點,那麽程式就會繼續執行,並且偵錯程式不會再控制程式。

  • 跨步(Step over) :執行下一條指令,但不會進入到一個函式中,快捷鍵 F10。

  • 步入(Step into) :快捷鍵 F11,和「下一步(Step)」類似,但在異步函式呼叫情況下表現不同,步入會進入到程式碼中並等待異步函式執行。

  • 步出(Step out) :繼續執行到當前函式的末尾,快捷鍵 Shift+F11,繼續執行程式碼並停止在當前函式的最後一行,當我們使用偶然地進入到一個巢狀呼叫,但是我們又對這個函式不感興趣時,我們想要盡可能的繼續執行到最後的時候是非常方便的。

  • 下一步(Step) :執行下一條語句,快捷鍵 F9,一次接一次地點選此按鈕,整個指令碼的所有語句會被逐個執行,下一步命令會忽略異步行為。

  • 啟用/禁用所有的斷點:這個按鈕不會影響程式的執行。只是一個批次操作斷點的開/關。

  • 察看(Watch) :顯示任意運算式的當前值

  • 呼叫棧(Call Stack) :顯示巢狀的呼叫鏈

  • 作用域(Scope) :顯示當前的變量

  • Local :顯示當前函式中的變量

  • Global :顯示全域變量

  • ② 條件斷點(Conditional breakpoint)

    給斷點添加條件,只有符合條件時,才會觸發斷點,條件斷點的顏色是橙色。

    ③ 日誌斷點(logpoint)

    當程式碼執行到這裏時,會在控制台輸出你的運算式,不會暫停程式碼執行,日誌斷點式粉紅色。

    debugger命令

    透過在程式碼中添加"debugger;"語句,當程式碼執行到該語句的時候就會自動斷點,之後的操作和在Sources面板添加斷點偵錯,唯一的區別在於偵錯完後需要刪除該語句。

    在開發中偶爾會遇到異步載入html片段(包含內嵌JS程式碼)的情況,而這部份JS程式碼在Sources樹中無法找到,因此無法直接在開發工具中直接添加斷點,那麽如果想給異步載入的指令碼添加斷點,此時"debugger;"就發揮作用了。


    3.2 反偵錯手段


    3.2.1 禁用開發者工具


    監聽是否開啟開發者工具,若開啟,則直接呼叫JavaScript的window.close( )方法關閉網頁

    ① 監聽F12按鍵、監聽Ctrl+Shift+I(Windows系統)組合鍵、監聽右鍵選單,監聽Ctrl+s禁止保存至本地,避免被Overrides。

    <script> //監聽F12、Ctrl+Shift+i、Ctrl+s document.onkeydown = function (event) { if (event.key === "F12") { window.close(); window.location = "about:blank"; } else if (event.ctrlKey && event.shiftKey && event.key === "I") {//此處I必須大寫 window.close(); window.location = "about:blank"; } else if (event.ctrlKey && event.key === "s") {//此處s必須小寫 event.preventDefault(); window.close(); window.location = "about:blank"; } }; //監聽右鍵選單 document.oncontextmenu = function () { window.close(); window.location = "about:blank"; };</script>


    ② 監聽視窗大小變化

    <script> var h = window.innerHeight, w = window.innerWidth; window.onresize = function () { if (h !== window.innerHeight || w !== window.innerWidth) { window.close(); window.location = "about:blank"; } }</script>


    ③ 利用Console.log

    <script> //控制台開啟的時候回呼方法 function consoleOpenCallback(){ window.close(); window.location = "about:blank"; return ""; } //立即執行函式,用來檢測控制台是否開啟 !function () { // 建立一個物件 let foo = /./; // 將其打印到控制台上,實際上是一個指標 console.log(foo); // 要在第一次打印完之後再重寫toString方法 foo.toString = consoleOpenCallback; }()</script>


    3.2.2 無限debugger反偵錯


    ① constructor

    <script> function consoleOpenCallback() { window.close(); window.location = "about:blank"; } setInterval(function () { const before = new Date(); (function(){}).constructor("debugger")(); // debugger; const after = new Date(); const cost = after.getTime() - before.getTime(); if (cost > 100) { consoleOpenCallback(); } }, 1000);</script>


    ② Function

    <script> setInterval(function () { const before = new Date(); (function (a) { return (function (a) { return (Function('Function(arguments[0]+"' + a + '")()')) })(a) })('bugger')('de'); // Function('debugger')(); // debugger; const after = new Date(); const cost = after.getTime() - before.getTime(); if (cost > 100) { consoleOpenCallback2(); } }, 1000);</script>

    有大佬寫了一個庫專門用來判斷是否開啟了開發者工具,可供參考使用:點選檢視>>


    3.3 反反偵錯手段


    3.3.1 禁用開發者工具

    針對判斷是否開啟開發者工具的破解方式很簡單,只需兩步就可以搞定。

    ① 將開發者工具以獨立視窗形式開啟

    ② 開啟開發者工具後再開啟網址

    3.3.2 無限debugger

    針對無限debugger反偵錯,有以下破解方法

    ① 直接使用dubbger指令的,可以在Chrome找到對應行(格式化後),右鍵行號,選擇Never pause here即可。

    ② 使用了constructor構造debugger的,只需在console中輸入以下程式碼後,點選F8(Resume script execution)回復js程式碼執行即可(直接點選小的藍色放行按鈕即可)。

    Function.prototype.constructor=function(){}


    ③ 使用了Function構造debugger的,只需在console中輸入以下程式碼。

    Function = function () {}


    3.4 總結


    JavaScript混淆加密使得程式碼更難以被反編譯和分析,從而提高了程式碼的安全性,攻擊者需要花費更多的時間和精力才能理解和分析程式碼,從而降低了攻擊者入侵的成功率,但它並不能完全保護程式碼不被反編譯和分析,如果攻擊者有足夠的時間和資源,他們仍然可以理解程式碼並找到其中的漏洞,道高一尺,魔高一丈,任何客戶端加密混淆都會被破解,只要用心都能解決,我們能做的就是拖延被破解的時間,所以盡量避免在前端程式碼中嵌入敏感資訊或業務邏輯。

    如喜歡本文,請點選右上角,把文章分享到朋友圈
    如有想了解學習的技術點,請留言給若飛安排分享

    因公眾號更改推播規則,請點「在看」並加「星標」 第一時間獲取精彩技術分享

    ·END·

    相關閱讀:

    作者:Luo Bingsong

    來源:vivo互聯網技術

    版權申明:內容來源網路,僅供學習研究,版權歸原創者所有。如有侵權煩請告知,我們會立即刪除並表示歉意。謝謝!

    架構師

    我們都是架構師!

    關註 架構師(JiaGouX),添加「星標」

    獲取每天技術幹貨,一起成為牛逼架構師

    技術群請 加若飛: 1321113940 進架構師群

    投稿、合作、版權等信箱: [email protected]