作者:vivo 互聯(lián)網(wǎng)安全團(tuán)隊(duì)- Luo Bingsong
前端代碼都是公開(kāi)的十籍,為了提高代碼的破解成本孔轴、保證JS代碼里的一些重要邏輯不被居心叵測(cè)的人利用剃法,需要使用一些加密和混淆的防護(hù)手段。
一路鹰、概念解析
1.1 什么是接口加密
如今這個(gè)時(shí)代贷洲,數(shù)據(jù)已經(jīng)變得越來(lái)越重要,網(wǎng)頁(yè)和APP是主流的數(shù)據(jù)載體晋柱,如果獲取數(shù)據(jù)的接口沒(méi)有設(shè)置任何的保護(hù)措施的話优构,數(shù)據(jù)就會(huì)被輕易地竊取或篡改。
除了數(shù)據(jù)泄露外雁竞,一些重要功能的接口如果沒(méi)有做好保護(hù)措施也會(huì)被惡意調(diào)用造成DDoS钦椭、條件競(jìng)爭(zhēng)等攻擊效果,比如如下幾個(gè)場(chǎng)景:
一些營(yíng)銷活動(dòng)類的Web頁(yè)面,領(lǐng)紅包彪腔、領(lǐng)券侥锦、投票、抽獎(jiǎng)等活動(dòng)方式很常見(jiàn)德挣。此類活動(dòng)對(duì)于普通用戶來(lái)說(shuō)應(yīng)該是“拼手氣”捎拯,而對(duì)于非正常用戶來(lái)說(shuō),可以通過(guò)直接刷活動(dòng)API接口的這種“作弊”方式來(lái)提升“手氣”盲厌。這樣對(duì)普通用戶來(lái)說(shuō)就很不公平。
所以對(duì)重要接口都會(huì)采用加密驗(yàn)簽的方式進(jìn)行保護(hù)祸泪,而驗(yàn)簽的加密邏輯大多數(shù)都通過(guò)JS代碼實(shí)現(xiàn)吗浩,所以保護(hù)JS代碼不被攻擊者竊取尤為重要。
1.2 為什么要保護(hù)JS代碼
JavaScript代碼運(yùn)行于客戶端
JavaScript代碼是公開(kāi)透明的
由于這兩個(gè)原因没隘,致使JavaScript代碼是不安全的懂扼,任何人都可以讀、分析右蒲、復(fù)制阀湿、盜用甚至篡改。
1.3 應(yīng)用場(chǎng)景
以下場(chǎng)景就通過(guò)特定的防護(hù)措施提高了攻擊成本:
某些網(wǎng)站會(huì)在頁(yè)面中使用JavaScript對(duì)數(shù)據(jù)進(jìn)行加密瑰妄,以保護(hù)數(shù)據(jù)的安全性和隱私性陷嘴,在爬取時(shí)需要通過(guò)解密JavaScript代碼才能獲取到數(shù)據(jù)。
某些網(wǎng)站的URL會(huì)有某個(gè)參數(shù)帶有一些看不太懂的長(zhǎng)串加密參數(shù)间坐,攻擊者要爬取的話就必須要知道這些參數(shù)是怎么構(gòu)造的灾挨,否則無(wú)法正確地訪問(wèn)該URL。
翻看網(wǎng)站的JavaScript源代碼竹宋,可以發(fā)現(xiàn)很多壓縮了或者看不太懂的字符劳澄,比如JavaScript文件名被編碼,JavaScript的文件內(nèi)容都?jí)嚎s成幾行蜈七,JavaScript變量也被修改成單個(gè)字符或者一些十六進(jìn)制的字符秒拔,所以我們不能輕易地根據(jù)JavaScript找出某些接口的加密邏輯。
1.4 涉及的技術(shù)
這些場(chǎng)景都是網(wǎng)站為了保護(hù)數(shù)據(jù)不被輕易抓取采取的措施飒硅,運(yùn)用的技術(shù)主要有:
接口加密技術(shù)
JavaScript壓縮砂缩、混淆和加密技術(shù)
二、技術(shù)原理
2.1 接口加密技術(shù)
數(shù)據(jù)和功能一般是通過(guò)服務(wù)器提供的接口來(lái)實(shí)現(xiàn)狡相,為了提升接口的安全性梯轻,客戶端會(huì)和服務(wù)端約定一種接口檢驗(yàn)方式,通常是各種加密和編碼算法尽棕,如Base64喳挑、Hex、MD5、AES伊诵、DES单绑、RSA等。
常用的數(shù)據(jù)接口都會(huì)攜帶一個(gè)sign參數(shù)用于權(quán)限管控:
① 客戶端和服務(wù)端約定一種接口校驗(yàn)邏輯曹宴,客戶端在每次請(qǐng)求服務(wù)端接口的時(shí)候附帶一個(gè)sign參數(shù)搂橙。② sign參數(shù)的邏輯自定義,可以由當(dāng)前時(shí)間戳信息笛坦、設(shè)備ID区转、日期、雙方約定好的秘鑰經(jīng)過(guò)一些加密算法構(gòu)造而成版扩。③ 客戶端根據(jù)約定的加密算法構(gòu)造sign废离,每次請(qǐng)求服務(wù)器的時(shí)候附帶上sign數(shù)。④ 服務(wù)端根據(jù)約定的加密算法和請(qǐng)求的數(shù)據(jù)對(duì)sign進(jìn)行校驗(yàn)礁芦,如果檢驗(yàn)通過(guò)蜻韭,才返回?cái)?shù)據(jù),否則拒絕響應(yīng)柿扣。
這就是一個(gè)比較簡(jiǎn)單的接口參數(shù)加密的實(shí)現(xiàn)肖方,如果有人想要調(diào)用這個(gè)接口的話,必須要破解sign的生成邏輯未状,否則是無(wú)法正常調(diào)用接口的俯画。
當(dāng)然上面的實(shí)現(xiàn)思路比較簡(jiǎn)單,還可以增加一些時(shí)間戳信息和訪問(wèn)頻次來(lái)增加時(shí)效性判斷司草,或使用非對(duì)稱加密提高加密的復(fù)雜程度活翩。
實(shí)現(xiàn)接口參數(shù)加密需要用到一些加密算法,客戶端和服務(wù)器都有對(duì)應(yīng)的SDK來(lái)實(shí)現(xiàn)這些加密算法翻伺,如JavaScript的crypto-js材泄、Python的hashlib、Crypto等等吨岭。如果是網(wǎng)頁(yè)且客戶端的加密邏輯是用JavaScript來(lái)實(shí)現(xiàn)的話拉宗,其源代碼對(duì)用戶是完全可見(jiàn)的,所以我們需要用壓縮辣辫、混淆旦事、加密的方式來(lái)對(duì)JavaScript代碼進(jìn)行一定程度的保護(hù)。
2.2 什么是壓縮
去除JavaScript代碼中不必要的空格急灭、換行等內(nèi)容姐浮,使源碼都?jí)嚎s為幾行內(nèi)容,降低代碼可讀性葬馋,同時(shí)可提高網(wǎng)站的加載速度卖鲤。
如果僅僅是去除空格換行這樣的壓縮方式肾扰,幾乎沒(méi)有任何防護(hù)作用,這種壓縮方式僅僅是降低了代碼的直接可讀性蛋逾,可以用IDE集晚、在線工具或Chrome輕松將JavaScript代碼變得易讀。
所以JavaScript壓縮技術(shù)只能在很小的程度上起到防護(hù)作用区匣,想提高防護(hù)的效果還得依靠JavaScript混淆和加密技術(shù)偷拔。
2.3 什么是混淆
使用變量混淆、字符串混淆亏钩、屬性加密莲绰、控制流平坦化、調(diào)試保護(hù)姑丑、多態(tài)變異等手段钉蒲,使代碼變得難以閱讀和分析,同時(shí)不影響代碼原有功能彻坛,是一種理想且實(shí)用的JS保護(hù)方案。
變量混淆:將變量名踏枣、方法名昌屉、常量名隨機(jī)變?yōu)闊o(wú)意義的亂碼字符串,降低代碼可讀性茵瀑,如轉(zhuǎn)成單個(gè)字符或十六進(jìn)制字符串间驮。
字符串混淆:將字符串陣列化集中放置,并進(jìn)行MD5或Base64編碼存儲(chǔ)马昨,使代碼中不出現(xiàn)明文字符串竞帽,可以避免使用全局搜索字符串的方式定位到入口點(diǎn)。
屬性加密:針對(duì)JavaScript對(duì)象的屬性進(jìn)行加密轉(zhuǎn)化鸿捧,隱藏代碼之間的調(diào)用關(guān)系屹篓,把key-value的映射關(guān)系混淆掉。
控制流平坦化:打亂函數(shù)原有代碼執(zhí)行流程及函數(shù)調(diào)用關(guān)系匙奴,使代碼邏輯變得混亂無(wú)序堆巧。
調(diào)試保護(hù):基于調(diào)試器特性,加入一些強(qiáng)制調(diào)試debug語(yǔ)句泼菌,無(wú)限debug谍肤、定時(shí)debug、debug關(guān)鍵字哗伯,使其在調(diào)試模式下難以順利執(zhí)行JavaScript代碼荒揣。
多態(tài)變異:JavaScript代碼每次被調(diào)用時(shí),代碼自身立刻自動(dòng)發(fā)生變異焊刹,變化為與之前完全不同的代碼系任,避免代碼被動(dòng)態(tài)分析調(diào)試恳蹲。
2.4 什么是加密
JavaScript加密是對(duì)JavaScript混淆技術(shù)防護(hù)的進(jìn)一步升級(jí),基本思路是將一些核心邏輯用C/C++語(yǔ)言來(lái)編寫(xiě)赋除,并通過(guò)JavaScript調(diào)用執(zhí)行阱缓,從而起到二進(jìn)制級(jí)別的防護(hù)作用,加密的方式主要有Emscripten和WebAssembly等举农。
1. Emscripten
Emscripten編譯器可以將C/C++代碼編譯成asm.js的JavaScript變體荆针,再由JavaScript調(diào)用執(zhí)行,因此某些JavaScript的核心功能可以使用C/C++語(yǔ)言實(shí)現(xiàn)颁糟。
2.WebAssembly
WebAssembly也能將C/C++代碼轉(zhuǎn)成JavaScript引擎可以運(yùn)行的代碼航背,但轉(zhuǎn)出來(lái)的代碼是二進(jìn)制字節(jié)碼,而asm.js是文本棱貌,因此運(yùn)行速度更快玖媚、體積更小,得到的字節(jié)碼具有和JavaScript相同的功能婚脱,在語(yǔ)法上完全脫離JavaScript今魔,同時(shí)具有沙盒化的執(zhí)行環(huán)境,利用WebAssembly技術(shù)障贸,可以將一些核心的功能用C/C++語(yǔ)言實(shí)現(xiàn)错森,形成瀏覽器字節(jié)碼的形式,然后在JavaScript中通過(guò)類似如下的方式調(diào)用:
這種加密方式更加安全篮洁,想要逆向或破解需要逆向WebAssembly涩维,難度極大。
2.5 工具介紹
2.5.1 壓縮混淆工具
- Uglifyjs(開(kāi)源):
用NodeJS編寫(xiě)的JavaScript壓縮工具袁波,是目前最流行的JS壓縮工具瓦阐,JQuery就是使用此工具壓縮,UglifyJS壓縮率高篷牌,壓縮選項(xiàng)多睡蟋,并且具有優(yōu)化代碼,格式化代碼功能枷颊。
jshaman是一個(gè)商業(yè)級(jí)工具薄湿,看了很多社區(qū)的評(píng)論,這個(gè)目前是最好的偷卧,可以在線免費(fèi)使用豺瘤,也可以購(gòu)買商業(yè)版。
開(kāi)源的js混淆工具听诸,原理比較簡(jiǎn)單坐求,通過(guò)特定的字符串加上下標(biāo)定位字符,再由這些字符替換源代碼晌梨,從而實(shí)現(xiàn)混淆桥嗤。
業(yè)界巨頭yahoo提供的一個(gè)前端壓縮工具须妻,通過(guò)java庫(kù)編譯css或js文件進(jìn)行壓縮
2.5.2 反混淆工具
jsbeautifier是一個(gè)為前端開(kāi)發(fā)人員制作的Chrome擴(kuò)展,能夠直接查看經(jīng)過(guò)壓縮的Javascript代碼泛领。
壓縮工具uglify對(duì)應(yīng)的解混淆工具荒吏。
用PHP編寫(xiě)的壓縮工具,可以混淆代碼保護(hù)知識(shí)產(chǎn)權(quán)渊鞋,產(chǎn)生的代碼兼容IE绰更、FireFox等常用瀏覽器,國(guó)內(nèi)大部分在線工具網(wǎng)站都采用這種算法壓縮锡宋。
三儡湾、前端安全對(duì)抗
3.1 前端調(diào)試手法
3.1.1 Elements
Elements 面板會(huì)顯示目前網(wǎng)頁(yè)中的 DOM、CSS 狀態(tài)执俩,且可以修改頁(yè)面上的 DOM 和 CSS徐钠,即時(shí)看到結(jié)果,省去了在編輯器修改役首、儲(chǔ)存尝丐、瀏覽器查看結(jié)果的流程。
有時(shí)候一些dom節(jié)點(diǎn)會(huì)嵌套很深衡奥,導(dǎo)致我們很難利用Element面板html代碼來(lái)找到對(duì)應(yīng)的節(jié)點(diǎn)爹袁。inspect(dom元素)可以讓我們快速跳轉(zhuǎn)到對(duì)應(yīng)的dom節(jié)點(diǎn)的html代碼上。
3.1.2 Console
Console對(duì)象提供了瀏覽器控制臺(tái)調(diào)試的接口杰赛,Console是一個(gè)對(duì)象,上面有很多方便的方法矮台。
console.log( ):最常用的語(yǔ)句乏屯,可以將變量輸出到瀏覽器的控制臺(tái)中,方便開(kāi)發(fā)者調(diào)用JS代碼
console.table( ):可用于打印obj/arr成表格
console.trace( ):可用于debugger堆棧調(diào)試瘦赫,方便查看代碼的執(zhí)行邏輯辰晕,看一些庫(kù)的源碼
console.count( ):打印標(biāo)簽被執(zhí)行了幾次,預(yù)設(shè)值是default确虱,可用在快速計(jì)數(shù)
console.countReset( ):用來(lái)重置含友,可用在計(jì)算單次行為的觸發(fā)的計(jì)數(shù)
console.group( )/console.groupEnd( ):
為了方便一眼看到自己的log,可以用console.group自定義message group標(biāo)簽校辩,還可以多層嵌套窘问,并用console.groupEnd來(lái)關(guān)閉Group。
3.1.3 JS斷點(diǎn)調(diào)試
JS斷點(diǎn)調(diào)試宜咒,即在瀏覽器開(kāi)發(fā)者工具中為JS代碼添加斷點(diǎn)惠赫,讓JS執(zhí)行到某一特定位置停住,方便開(kāi)發(fā)者對(duì)該處代碼段進(jìn)行分析與邏輯處理故黑。
** Sources面板**
① 普通斷點(diǎn)(breakpoint)
給一段代碼添加斷點(diǎn)的流程是:"F12(Ctrl + Shift + I)打開(kāi)開(kāi)發(fā)工具"->"點(diǎn)擊Sources菜單"->"左側(cè)樹(shù)中找到相應(yīng)文件"→"點(diǎn)擊行號(hào)列"即完成在當(dāng)前行添加/刪除斷點(diǎn)操作儿咱。當(dāng)斷點(diǎn)添加完畢后庭砍,刷新頁(yè)面JS執(zhí)行到斷點(diǎn)位置停住,在Sources界面會(huì)看到當(dāng)前作用域中所有變量和值混埠。
恢復(fù)(Resume): 恢復(fù)按鈕(第一個(gè)按鈕)怠缸,繼續(xù)執(zhí)行,快捷鍵 F8钳宪,繼續(xù)執(zhí)行揭北,如果沒(méi)有其他的斷點(diǎn),那么程序就會(huì)繼續(xù)執(zhí)行使套,并且調(diào)試器不會(huì)再控制程序罐呼。
跨步(Step over):運(yùn)行下一條指令,但不會(huì)進(jìn)入到一個(gè)函數(shù)中侦高,快捷鍵 F10嫉柴。
步入(Step into):快捷鍵 F11,和“下一步(Step)”類似奉呛,但在異步函數(shù)調(diào)用情況下表現(xiàn)不同计螺,步入會(huì)進(jìn)入到代碼中并等待異步函數(shù)執(zhí)行。
步出(Step out):繼續(xù)執(zhí)行到當(dāng)前函數(shù)的末尾瞧壮,快捷鍵 Shift+F11登馒,繼續(xù)執(zhí)行代碼并停止在當(dāng)前函數(shù)的最后一行,當(dāng)我們使用偶然地進(jìn)入到一個(gè)嵌套調(diào)用咆槽,但是我們又對(duì)這個(gè)函數(shù)不感興趣時(shí)陈轿,我們想要盡可能的繼續(xù)執(zhí)行到最后的時(shí)候是非常方便的。
下一步(Step):運(yùn)行下一條語(yǔ)句秦忿,快捷鍵 F9麦射,一次接一次地點(diǎn)擊此按鈕,整個(gè)腳本的所有語(yǔ)句會(huì)被逐個(gè)執(zhí)行灯谣,下一步命令會(huì)忽略異步行為潜秋。
啟用/禁用所有的斷點(diǎn):這個(gè)按鈕不會(huì)影響程序的執(zhí)行。只是一個(gè)批量操作斷點(diǎn)的開(kāi)/關(guān)胎许。
察看(Watch):顯示任意表達(dá)式的當(dāng)前值
調(diào)用棧(Call Stack):顯示嵌套的調(diào)用鏈
作用域(Scope):顯示當(dāng)前的變量
Local:顯示當(dāng)前函數(shù)中的變量
Global:顯示全局變量
② 條件斷點(diǎn)(Conditional breakpoint)
給斷點(diǎn)添加條件峻呛,只有符合條件時(shí),才會(huì)觸發(fā)斷點(diǎn)辜窑,條件斷點(diǎn)的顏色是橙色钩述。
③ 日志斷點(diǎn)(logpoint)
當(dāng)代碼執(zhí)行到這里時(shí),會(huì)在控制臺(tái)輸出你的表達(dá)式穆碎,不會(huì)暫停代碼執(zhí)行切距,日志斷點(diǎn)式粉紅色。
debugger命令
通過(guò)在代碼中添加"debugger;"語(yǔ)句惨远,當(dāng)代碼執(zhí)行到該語(yǔ)句的時(shí)候就會(huì)自動(dòng)斷點(diǎn)谜悟,之后的操作和在Sources面板添加斷點(diǎn)調(diào)試话肖,唯一的區(qū)別在于調(diào)試完后需要?jiǎng)h除該語(yǔ)句。
在開(kāi)發(fā)中偶爾會(huì)遇到異步加載html片段(包含內(nèi)嵌JS代碼)的情況葡幸,而這部分JS代碼在Sources樹(shù)中無(wú)法找到最筒,因此無(wú)法直接在開(kāi)發(fā)工具中直接添加斷點(diǎn),那么如果想給異步加載的腳本添加斷點(diǎn)蔚叨,此時(shí)"debugger;"就發(fā)揮作用了床蜘。
3.2 反調(diào)試手段
3.2.1 禁用開(kāi)發(fā)者工具
監(jiān)聽(tīng)是否打開(kāi)開(kāi)發(fā)者工具,若打開(kāi)蔑水,則直接調(diào)用JavaScript的window.close( )方法關(guān)閉網(wǎng)頁(yè)
① 監(jiān)聽(tīng)F12按鍵邢锯、監(jiān)聽(tīng)Ctrl+Shift+I(Windows系統(tǒng))組合鍵、監(jiān)聽(tīng)右鍵菜單搀别,監(jiān)聽(tīng)Ctrl+s禁止保存至本地丹擎,避免被Overrides。
<script>
//監(jiān)聽(tīng)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必須大寫(xiě)
window.close();
window.location = "about:blank";
} else if (event.ctrlKey && event.key === "s") {//此處s必須小寫(xiě)
event.preventDefault();
window.close();
window.location = "about:blank";
}
};
//監(jiān)聽(tīng)右鍵菜單
document.oncontextmenu = function () {
window.close();
window.location = "about:blank";
};
</script>
② 監(jiān)聽(tīng)窗口大小變化
<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>
//控制臺(tái)打開(kāi)的時(shí)候回調(diào)方法
function consoleOpenCallback(){
window.close();
window.location = "about:blank";
return "";
}
//立即運(yùn)行函數(shù),用來(lái)檢測(cè)控制臺(tái)是否打開(kāi)
!function () {
// 創(chuàng)建一個(gè)對(duì)象
let foo = /./;
// 將其打印到控制臺(tái)上榜苫,實(shí)際上是一個(gè)指針
console.log(foo);
// 要在第一次打印完之后再重寫(xiě)toString方法
foo.toString = consoleOpenCallback;
}()
</script>
3.2.2 無(wú)限debugger反調(diào)試
① 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>
有大佬寫(xiě)了一個(gè)庫(kù)專門(mén)用來(lái)判斷是否打開(kāi)了開(kāi)發(fā)者工具护戳,可供參考使用:點(diǎn)擊查看>>
3.3 反反調(diào)試手段
3.3.1 禁用開(kāi)發(fā)者工具
針對(duì)判斷是否打開(kāi)開(kāi)發(fā)者工具的破解方式很簡(jiǎn)單,只需兩步就可以搞定垂睬。
① 將開(kāi)發(fā)者工具以獨(dú)立窗口形式打開(kāi)
② 打開(kāi)開(kāi)發(fā)者工具后再打開(kāi)網(wǎng)址
3.3.2 無(wú)限debugger
針對(duì)無(wú)限debugger反調(diào)試媳荒,有以下破解方法
① 直接使用dubbger指令的,可以在Chrome找到對(duì)應(yīng)行(格式化后)驹饺,右鍵行號(hào)钳枕,選擇Never pause here即可。
② 使用了constructor構(gòu)造debugger的逻淌,只需在console中輸入以下代碼后么伯,點(diǎn)擊F8(Resume script execution)回復(fù)js代碼執(zhí)行即可(直接點(diǎn)擊小的藍(lán)色放行按鈕即可)疟暖。
Function.prototype.constructor=function(){}
③ 使用了Function構(gòu)造debugger的卡儒,只需在console中輸入以下代碼。
Function = function () {}
3.4 總結(jié)
JavaScript混淆加密使得代碼更難以被反編譯和分析俐巴,從而提高了代碼的安全性骨望,攻擊者需要花費(fèi)更多的時(shí)間和精力才能理解和分析代碼,從而降低了攻擊者入侵的成功率欣舵,但它并不能完全保護(hù)代碼不被反編譯和分析擎鸠,如果攻擊者有足夠的時(shí)間和資源,他們?nèi)匀豢梢岳斫獯a并找到其中的漏洞缘圈,道高一尺劣光,魔高一丈袜蚕,任何客戶端加密混淆都會(huì)被破解,只要用心都能解決绢涡,我們能做的就是拖延被破解的時(shí)間牲剃,所以盡量避免在前端代碼中嵌入敏感信息或業(yè)務(wù)邏輯。