眾所周知,有效保護 Web 應用程式和網站需要使用 JavaScript從瀏覽器收集客戶端資料。這些數據通常包括裝置和瀏覽器特性、使用者偏好設定(指紋辨識)以及反映使用者與裝置互動的數據,例如滑鼠移動、觸控和按鍵操作(遙測)。
網路安全從業者和供應商透過各種檢測方法(從簡單規則到高級人工智慧模型)處理數據,以驗證合法請求源自人類控制的合法設備的可能性。
整合不同的數據點也有助於區分使用者並評估其隨時間推移的活動。這是機器人管理和詐欺偵測產品用來偵測諸如憑證填充、帳戶接管、濫用開戶和內容抓取等攻擊的核心原則。
資料收集完整性
確保資料的真實性和完整性是準確評估使用者與網站互動以及標記威脅的關鍵。既然知道客戶端執行的任何操作都可能被篡改和操縱,又如何能斷言資料的真實性和完整性呢?
在執行客戶端 JavaScript 程式碼時,有兩個原因需要確保程式碼受到良好的保護。
1.JavaScript程式碼是組織智慧財產權的一部分。必須盡可能地保護它免受威脅行為者和競爭對手的攻擊。
2.資料完整性對於正確理解環境及其風險因素至關重要。受保護的 JavaScript可確保資料的可信度,因為資料是透過執行腳本真實收集的,並且未經任何操縱或轉換。
如何保護客戶端程式碼並確保資料真實性
與安全領域的其他方面一樣,沒有單一的解決方案可以徹底解決這個問題。在本篇部落格文章中,我們將介紹Akamai 用於保護 JavaScript程式碼、強制執行程式碼並確保所收集資料真實性的方法,包括:
代碼混淆
資料完整性檢查
虛擬機器混淆
誤導性和額外代碼插入
JavaScript 程式碼輪換
動態場旋轉
JavaScript建置管道和資料驗證
如果您決定遵循類似的做法來保護自己的程式碼,我們建議根據您的團隊、組織和技術堆疊的需求結合使用這些方法。
代碼混淆
混淆是保護 JavaScript 程式碼最常用的方法之一。混淆會使程式碼更難以追蹤和理解。
良好的開發實務建議,函數和變數的命名應盡可能具有描述性,且程式碼結構應符合邏輯,以便於偵錯和維護。雖然這是一種非常有效的省時省力的做法,但乾淨的程式碼很容易成為逆向工程的目標。
當使用混淆技術時,這些合理的開發實踐就會被打破,原本具有描述性名稱的變數和函數會被替換成隨機變數和函數。它們可能會被重新排序和編碼,部分邏輯也可能被拆分。
Web瀏覽器仍然可以順利執行程式碼,結果也一樣。然而,任何試圖對程式碼進行逆向工程的人都會面臨更大的挑戰。
開發人員仍然使用結構良好的程式碼進行維護和功能增強。新版本發布後,程式碼會先經過混淆引擎的運行,然後再發布。市面上有多種免費/開源和商業產品,例如Code Beautify、 JScrambler和 Digital.ai,可以快速輕鬆地對 JavaScript程式碼進行混淆。
圖 1 是指紋辨識時常用的簡單 JavaScript函數範例,旨在提取各種裝置特徵,並在混淆之前顯示。
function getDeviceInfo() {
return {
userAgent: navigator.userAgent,
hardwareConcurrency: navigator.hardwareConcurrency || "unknown",
screenOrientation: screen.orientation.type,
};
}
圖1:混淆前的原始程式碼
您會發現,原始程式碼非常容易理解。即使是程式設計知識有限的人也能理解其預期目的,並理解它是如何實現目標的。
Code Beautify線上工具執行後的同一個JavaScript函數。
(function(_0xbf521e,_0x43c80b){var
_0x4ad763=_0x3e09,_0x18fc85=_0xbf521e();while(!![]){try{var_0x40d2a7=parseInt(_0x4ad763(0xfc))/(0x18d1+-0xe6d+-0xa63)+-parseInt(_0x4ad763(0xf6))/(0x2*-0x7e4+0x171a+-0x750)+-parseInt(_0x4ad763(0xfb))/(-0x2e7*-0xb+0x6b*0x1f+-0x2cdf)*(parseInt(_0x4ad763(0xef))/(0x40f*-0x4+-0x897+0x18d7))+-parseInt(_0x4ad763(0xf3))/(0x3*-0xb5f+0x462+0x1dc*0x10)*(parseInt(_0x4ad763(0xf0))/(-0xb87*-0x1+0x18e8+-0x3*0xc23))+-parseInt(_0x4ad763(0xfa))/(0x2258+0x8f7+-0x2b48)*(-parseInt(_0x4ad763(0xee))/(0x3e9+-0xe93+0xab2))+parseInt(_0x4ad763(0xf1))/(0x1*-0x81e+0x525*-0x5+0x4*0x878)+parseInt(_0x4ad763(0xed))/(-0x59*-0x1f+0x779+-0x6f*0x2a);if(_0x40d2a7===_0x43c80b)break;else
_0x18fc85'push';}catch(_0x4460fc){_0x18fc85'push';}}}(_0x1950,-0x1f*-0x38cb+0x17f2fa+-0x10aebf));function
getDeviceInfo(){var
_0x7a196=_0x3e09,_0x52340e={'VEDsL':_0x7a196(0xf8)};return{'userAgent':navigator[_0x7a196(0xf4)],'hardwareConcurrency':navigator[_0x7a196(0xf2)+_0x7a196(0xfd)]||_0x52340e[_0x7a196(0xf5)],'screenOrientation':screen[_0x7a196(0xf9)+'n'][_0x7a196(0xf7)]};}function
_0x3e09(_0x56cbb3,_0x1167d0){var _0xddc250=_0x1950();return
_0x3e09=function(_0x363b57,_0x27d74c){_0x363b57=_0x363b57-(-0x6d9+0x1316*0x1+-0xb50);var
_0x1b2eec=_0xddc250[_0x363b57];return
_0x1b2eec;},_0x3e09(_0x56cbb3,_0x1167d0);}function _0x1950(){var
_0x1d7105=['ncurrency','20162890GviEyp','2488DLGTpn','4rCTHCm','65154TKsGUe','7673175smCphy','hardwareCo','670lOXWEG','userAgent','VEDsL','1749116JlgXKK','type','unknown','orientatio','12971xihUJr','2027775PnQRTc','487370FufNiT'];_0x1950=function(){return
_0x1d7105;};return _0x1950();}
圖 2:混淆程式碼(透過 Code Beautify)
即便僅僅因為程式碼長度,混淆後的程式碼顯然更難理解。程式碼看起來很複雜,但逆轉這些簡單混淆技術的方法確實存在,而且威脅行為者也非常了解。但這至少提高了門檻,可以嚇阻那些經驗最淺、知識最匱乏的威脅行為者。
安全戰鬥的一半是耗盡威脅行為者的精力和/或使針對您的組織的前景變得沒有吸引力,這取決於策劃一次成功攻擊所需的感知或實際努力。
資料完整性檢查
如我們所見,程式碼混淆是一個很好的起點,但僅靠它不足以震懾有動機的威脅行為者,因為存在反混淆方法和工具可以將程式碼還原為原始格式。除了混淆方法之外,實作額外的程式碼和資料完整性檢查功能可以進一步保護所收集資訊的完整性。
程式碼和資料完整性檢查是新增到程式碼各個位置的小函數,用於驗證腳本產生的輸出是否合法。這些檢查通常使用多個變量,包括現有核心JavaScript 函數的輸出以及特定於使用者會話的唯一種子,以產生輔助輸出。
圖 3是一個函數範例,該函數以三個變數作為輸入,在一個簡單的數學公式和雜湊函數中使用這些變量,並傳回結果。變數a 和 b 可以對應兩個核心函數的輸出,變數 c可以是一個唯一的種子。在此範例中,所有屬性都必須是數值。
function IntegrityCheck(a, b, c) {
const mathResult = a + b * c;
const stringResult = String(mathResult);
let hash = 0;
for (let i = 0; i < stringResult.length; i++) {
hash = (hash * 31 + stringResult.charCodeAt(i)) >>> 0;
}
return hash;
}
圖 3:具有多個變數以確保資料完整性的程式碼範例
更具體地說,
screen.colorDepth和navigator.hardwareConcurrency這兩個傳回數值的屬性可以用作圖3 中簡單函數中的變數a 和b。該函數實際上並不局限於傳回數值的屬性,因為任何值都可以在輸入到完整性檢查函數之前進行雜湊處理並轉換為整數。我們這樣做只是為了方便我們舉一個簡單的例子。
為了多樣性,某些完整性檢查函數可能會對核心函數的輸出進行雜湊,如圖 4中的範例所示。
import { createHash } from 'crypto';
function hashTwoVariables(a, b) {
const concatenatedString = String(a) + String(b);
const hash = createHash('sha256').update(concatenatedString).digest('hex');
return hash;
}
圖 4:哈希輸出範例
可能有數十個這樣的小函數,每個函數執行不同的操作,並使用散佈在整個程式碼中的核心函數的不同輸出,以保護關鍵資料點。作為最終檢查,您還可以「簽署」整個有效載荷,包括所有指紋和行為數據,以及各個完整性檢查函數的結果。一種方法是對整個有效載荷進行哈希處理,然後比較初始輸出。如果發送方和接收方的雜湊值匹配,則有效載荷被視為安全且未被篡改。
虛擬機器混淆
這些簡單的完整性檢查函數無法暴露,也無法透過簡單的混淆方法隱藏。這時,更高級的虛擬機器(VM)混淆技術就派上用場了,它讓威脅行為者更難理解底層發生了什麼以及如何產生有效的有效載荷。
VM混淆將程式碼轉換為虛擬機器字節碼:機器可以解釋的東西,但對於威脅行為者來說,逆向工程則更具挑戰性。
一些供應商提供虛擬機器混淆方法,但虛擬機器混淆並不總是支援所有類型的函數邏輯。使用虛擬機器混淆時,請遵循供應商的指南,並對程式碼進行徹底的回歸測試。
回歸測試通常是一項很好的實踐,不僅適用於虛擬機器混淆,也值得作為安全例程的一部分來實施。然而,考慮到該方法的程式碼輸出非常複雜,迴歸測試與虛擬機器混淆結合使用時尤其有用。
誤導性和額外代碼插入
為了讓試圖逆向程式碼的威脅者更具挑戰性,研究人員額外添加了一層程式碼,這些程式碼對核心邏輯沒有任何實際用途。這樣做的目的是讓威脅者偏離正軌,挫敗他們,並迫使他們放棄努力。
同樣,您可以考慮改變完整性檢查函數的結構,以增加反混淆和逆向工程的難度。實現這一點的一種方法是開發幾個結構不同但等效的函數,它們可以產生相同的輸出。
功能相同但結構不同的函數在 VM混淆後將導致函數的編碼不同,使程式碼的逆向工程變得更加複雜。
圖 5 是三個此類函數的範例,它們始終返回相同的輸出,但略有不同。
function IntegrityCheck_1(a, b) {
return a + b * 1;
}
function IntegrityCheck_2(a, b) {
return a + 0 + b;
}
function IntegrityCheck_3(a, b, c) {
return a + b + c * 0;
}
圖 5:實現相同輸出的不同程式碼的三個範例
JavaScript 程式碼輪換
擁有誤導性程式碼、高階混淆和完整性檢查固然很好,但威脅行為者可能非常頑固——只要投入時間、精力和技能,任何停滯的程式碼都不可能被逆向工程。除非我們限制腳本的有效性。
想像一下,產生數千個功能相同的程式碼的唯一迭代,每個迭代都針對每個新的JavaScript 程式碼版本設定不同的完整性檢查函數。每個迭代僅在 10 到 20分鐘內有效,並且有控制措施強制客戶端定期重新加載新的迭代,從而使舊迭代很快過時並失效。
這種方法的目標是用複雜性壓倒威脅行為者並超越他們的效率,這樣他們別無選擇,只能透過瀏覽器執行JavaScript,並且不知道程式碼的作用。
動態場旋轉
程式碼可能難以閱讀和解讀,但通常可以透過檢查輸出以及收集和發送的資料來推斷其用途。傳送到伺服器的某些資訊可能顯而易見,尤其是有關裝置和瀏覽器特性等細節。
布林值的函數或傳回整數的完整性檢查函數,推斷其意圖會更加困難。
使有效載荷結構更難預測、對威脅行為者更令人困惑的一種方法是更改用於報告每個收集的資料點的欄位名稱,以及它們在每次迭代中在有效載荷中的相對位置。
正如我們所討論的,每次 JavaScript迭代都有一組獨特的程式碼完整性檢查。此外,有效載荷將使用不同的欄位名稱,並且給定資料點的位置會隨著每次迭代而變化。
欄位名稱及其位置是在 JavaScript建置時根據預定義演算法定義的,處理資料的伺服器也可以執行該演算法來檢索對於在正確位置準確檢測機器人和詐欺行為至關重要的各種資訊。
圖 6
展示了每個字段及其位置在每次迭代中的變化。字段名稱必須不具描述性,以使其最不顯眼。
Payload Iteration #1
mx01: [user-agent]
mx02: [display-mode]
mx03: [hardconcur]
mx04: [pixelDepth]
mx05: [language]
mx06: [WebGL_Rend]
mx07: [intg_chck_1]
Payload Iteration #2
yw01: [display-mode]
yw02: [intg_chck_1]
yw03: [user-agent]
yw04: [pixelDepth]
yw05: [hardconcur]
yw06: [WebGL_Rend]
yw07: [language]
Payload Iteration #3
za01: [language]
za02: [WebGL_Rend]
za03: [hardconcur]
za04: [pixelDepth]
za05: [intg_chck_1]
za06: [user-agent]
za07: [display-mode]
圖 6:欄位名稱迭代範例
由於輸出中只有七個欄位(如上例所示),因此很容易發現從一次迭代到另一次迭代的變化,但想像一下在收集並返回數百個資料點時這樣做。
JavaScript建置管道和資料驗證
用於保護 JavaScript程式碼並確保所收集資料完整性的各種方法都需要開發複雜的建置流程和發布流程。首先,開發人員將更新原始且格式良好的JavaScript 文件,測試其功能並執行回歸測試。
接下來,開發人員將使用演算法來產生數千次迭代,這將產生具有不同特點的獨特版本:
資料完整性檢查函數會根據核心
JavaScript、所使用的數學/雜湊函數以及它們在整體邏輯中的相對位置來改變資料點
一組誤導或未使用的代碼
有效載荷輸出欄位名稱
有效載荷輸出欄位順序
一旦產生這些唯一元件,JavaScript 檔案迭代就會經歷以下過程:
透過虛擬機器混淆資料完整性檢查和其他關鍵功能
混淆整體程式碼
將迭代上傳到 Web 伺服器
所有迭代產生並上傳後,必須在生產環境中啟用新的 JavaScript集。此更改需要與運行機器人的伺服器和接收資料的詐欺檢測引擎協調。它必須運行JavaScript 建置系統中使用的部分演算法,才能:
驗證客戶端發送的是當前 JavaScript 迭代的有效負載,而不是過時的有效負載
根據產生有效載荷的 JavaScript 迭代來解析有效載荷的不同字段
透過運行等效函數來驗證程式碼完整性檢查值
經過最終混淆處理的產品,必須在發布前的預生產階段進行全面的端到端測試,以確保所有組件同步並產生預期結果。這需要為JavaScript 建立一個略顯複雜的建置工作流程。
儘管如此,當其內容必須保護起來,不讓好奇的競爭對手和威脅行為者獲取,並且其輸出會影響互聯網用戶及其訪問的網站的安全時,這種努力就變得值得了。
結論
必須保護在客戶端運行的 JavaScript程式碼(用於收集指紋和遙測資料)以及用於檢測機器人和詐欺的自訂邏輯。目前有多種策略可以保護程式碼和數據,但僅實施一兩種策略只能提供有限的保護,以抵禦最複雜的威脅行為者。
保護客戶端程式碼及其有效負載需要複雜的策略,涉及多層防禦和技術,包括程式碼混淆、誤導性或未使用的程式碼、與虛擬機器混淆相結合的程式碼完整性檢查功能、隨機化有效負載結構以使其更難預測,以及定期更新程式碼。
圖 7 中的等式總結了為確保有效保護而製定的整體策略組合的複雜性。
[JS Code obfuscation[
+ Misleading code
+ unused code
+ VM Obfuscation [code integrity check]
+ unique field names
+ field relative position shift]
x [Number of unique iterations]
+ Limited version validity (10 minutes)
+ Force JS reload]
圖 7:JavaScript 保護策略方程
最終,這種組合會強制客戶端執行JavaScript,從而減少篡改資料和繞過偵測引擎的機會。為了減少開發工作量,強烈建議針對一些最複雜的步驟(例如虛擬機器混淆)使用商業解決方案。然而,某些策略(例如程式碼完整性檢查、誤導性程式碼片段和多次迭代)應該在內部建置和維護,以便在威脅行為者建構的反混淆器可用時提供保護。
歡迎關注Akamai,通過定期更新的文章瞭解更多與Web、安全、雲計算、邊緣計算有關的資訊和見解。