目錄
一、逆向基礎
1.1 語法基礎
1.2 作用域
1.3 窗口對象屬性
1.4 事件
二、瀏覽器控制臺
2.1 Network
Network-Headers
Network-Header-General
Network-Header-Response Headers
Network-Header-Request Headers
2.2 Sources
2.3 Application
2.4 Console
三、加密參數(shù)的定位方法
3.1 巧用搜索
3.2 堆棧調(diào)試
3.3 控制臺調(diào)試
3.4 監(jiān)聽XHR
3.5 事件監(jiān)聽
3.6 添加代碼片
3.7 Hook
四、常見的壓縮和混淆
4.1 JavaScript壓縮
4.2 JavaScript混淆
4.3 javascript-obfuscator示例
4.3.1 代碼壓縮
4.3.2 變量名混淆
4.3.3 字符串混淆
4.3.4 代碼自我保護
4.3.5 控制流平坦化
4.3.6 無用代碼注入
4.3.7 對象鍵名替換
4.3.8 禁用控制臺輸出
4.3.9 調(diào)試保護
4.3.10 域名鎖定
4.3.11特殊編碼
五、常見的編碼和加密
5.1 base64
5.2 MD5
5.3 SHA1
5.4 HMAC
5.5 DES
5.5 AES
5.6 RSA
一、逆向基礎
1.1 語法基礎
Js調(diào)試相對方便,通常只需要chrome或者一些抓包工具、擴展插件,就能順利的完成逆向分析。但是Js的弱類型和語法多樣,各種閉包,逗號表達式等語法讓代碼可讀性變得不如其他語言順暢。所以需要學習一下基礎語法。
-
基本數(shù)據(jù)類型
字符串 | String |
---|---|
數(shù)字 | Number |
布爾 | Boolean |
空值 | Null |
未定義 | Undefined |
獨一無二的值 | Symbol |
-
引用數(shù)據(jù)類型
對象 | Object |
---|---|
數(shù)組 | Array |
函數(shù) | Function |
-
語句標識符
在條件為true時重復執(zhí)行 | do……while |
---|---|
在條件為true時執(zhí)行 | while |
循環(huán)遍歷 | for |
條件判斷 | if……else |
根據(jù)情況執(zhí)行代碼塊 | switch |
退出循環(huán) | break |
異常捕獲 | try……catch……finally |
拋出異常 | Throw |
聲明固定值的變量 | const |
聲明類 | class |
停止函數(shù)并返回 | return |
聲明塊作用域的變量 | let |
聲明變量 | var |
斷點調(diào)試 | debugger |
當前所屬對象 | this |
-
算數(shù)運算符
加 | + |
---|---|
減 | - |
乘 | * |
除 | / |
取余 | % |
累加 | ++ |
遞減 | -- |
-
比較運算符
等于 | == |
---|---|
相等值或者相等類型 | === |
不等于 | != |
不相等值或者不相等類型 | !== |
大于 | > |
小于 | < |
大于等于 | >= |
小于等于 | <= |
在JavaScript中將數(shù)字存儲為64位浮點數(shù),但所有按位運算都以32位二進制數(shù)執(zhí)行。在執(zhí)行位運算之前,JavaScript將數(shù)字轉(zhuǎn)換位32位有符號整數(shù)。執(zhí)行按位操作后,結(jié)果將轉(zhuǎn)換回64位JavaScript數(shù)。
Javascript 函數(shù)
JavaScript 中的函數(shù)是頭等公民,不僅可以像變量一樣使用它,同時它還具有十分強大的抽象能力
定義函數(shù)的 2 種方式
在JavaScript 中,定義函數(shù)的方式如下:
function abs(x) {
if (x >= 0) {
return x;
} else {
return -x;
}
}
上述 abs() 函數(shù)的定義如下:
-
function 指出這是一個函數(shù)定義;
-
abs 是函數(shù)的名稱;
-
(x) 括號內(nèi)列出函數(shù)的參數(shù),多個參數(shù)以,分隔;
-
{……} 之間的代碼是函數(shù)體,可以包含若干語句,甚至可以沒有任何語句。
請注意,函數(shù)體內(nèi)部的語句在執(zhí)行時,一旦執(zhí)行到 return 時,函數(shù)就執(zhí)行完畢,并將結(jié)果返回。因此,函數(shù)內(nèi)部通過條件判斷和循環(huán)可以實現(xiàn)非常復雜的邏輯。
如果沒有 return 語句,函數(shù)執(zhí)行完畢后也會返回結(jié)果,只是結(jié)果為 undefined。
由于JavaScript的函數(shù)也是一個對象,上述定義的 abs()
函數(shù)實際上是一個函數(shù)對象,而函數(shù)名 abs
可以視為指向該函數(shù)的變量。
因此,第二種定義函數(shù)的方式如下:
var abs = function (x) {
if (x >= 0) {
return x;
} else {
return -x;
}
};
在這種方式下,function(x){……} 是一個匿名函數(shù),它沒有函數(shù)名。但是,這個匿名函數(shù)賦值給了變量 abs,所以,通過變量 abs 就可以調(diào)用該函數(shù)。
注意:上述兩種定義 完全等價 ,第二種方式按照完整語法需要在函數(shù)體末尾加一個 ;,表示賦值語句結(jié)束。( 加不加都一樣,如果按照語法完整性要求,需要加上)
調(diào)用函數(shù)時,按順序傳入?yún)?shù)即可:
abs(10); // 返回10
abs(-9); // 返回9
由于JavaScript 允許傳入任意個參數(shù)而不影響調(diào)用,因此傳入的參數(shù)比定義的參數(shù)多也沒有問題,雖然函數(shù)內(nèi)部并不需要這些參數(shù):
abs(10, 'blablabla'); // 返回10
abs(-9, 'haha', 'hehe', null); // 返回9
傳入的參數(shù)比定義的少也沒有問題:
abs(); // 返回NaN
此時 abs(s) 函數(shù)的參數(shù) x 將收到 undefined,計算結(jié)果為NaN。要避免收到undefined,可以對參數(shù)進行檢查:
function abs(x) {
if (typeof x !== 'number') {
throw 'Not a number'; // 停止并拋出錯誤信息
}
if (x >= 0) {
return x;
} else {
return -x;
}
}
1.2 作用域
Js中有一個被稱為作用域的特性。作用域是在運行時代碼中的某些特定部分中變量、函數(shù)和對象的可訪問性。換句話說,作用域決定了代碼區(qū)塊中變量和其他資源的可見性。作用域最大的用處就是隔離變量,不同作用域下同名變量不會有沖突。
Js的作用域分為三種:全局作用域、函數(shù)作用域、塊級作用域。全局作用域可以讓用戶在任何位置進行調(diào)用,需要注意的是最外層函數(shù)和在最外層函數(shù)外面定義的變量擁有全局作用域,所有未定義直接賦值的變量會自動聲明為擁有全局作用域,所有window對象的屬性也擁有全局作用域。函數(shù)作用域也就是說只有在函數(shù)內(nèi)部可以被訪問,當然函數(shù)內(nèi)部是可以訪問全局作用域的。塊級作用域則是在if和switch的條件語句或for和while的循環(huán)語句中,塊級作用域可通過新增命令let和const聲明,所聲明的變量在指定的作用域外無法被訪問。
1.3 窗口對象屬性
我們總結(jié)了瀏覽器window的常見屬性和方法。因為很多環(huán)境監(jiān)測都是基于這些屬性和方法的,在補環(huán)境前,需要了解window對象的常用屬性和方法。
-
Window
-
Window對象表示瀏覽器當前打開的窗口。
-
Document對象 | document |
---|---|
History對象 | history |
Location對象 | location |
Navigator對象 | navigator |
Screen對象 | screen |
按照指定的像素值來滾動內(nèi)容 | scrollBy() |
把內(nèi)容滾動到指定的坐標 | scrollTo() |
定時器 | setInterval() |
延時器 | setTimeout() |
彈出警告框 | alert() |
彈出對話框 | prompt() |
打開新頁面 | open() |
關閉頁面 | close() |
-
Document
-
載入瀏覽器的HTML文檔。
-
<body>元素 | body |
---|---|
當前cookie | cookie |
文檔域名 | domain |
文檔最后修改日期和時間 | lastModified |
訪問來源 | referrer |
文檔標題 | title |
當前URL | URL |
返回指定id的引用對象 | getElementById() |
返回指定名稱的對象集合 | getElementByName() |
返回指定標簽名的對象集合 | getElementByTagName() |
打開流接收輸入輸出 | open() |
向文檔輸入 | write() |
-
Navigator
-
Navigator對象包含的屬性描述了當前使用的瀏覽器,可以使用這些屬性進行平臺專用的配置。
-
用戶代理 | userAgent |
---|---|
瀏覽器代碼名 | AppCodeName |
瀏覽器名稱 | AppName |
瀏覽器版本 | AppVersion |
瀏覽器語言 | browserLanguage |
指明是否啟用cookie的布爾值 | cookieEnabled |
瀏覽器系統(tǒng)的cpu等級 | cpuClass |
是否處于脫機模式 | onLine |
瀏覽器的操作系統(tǒng)平臺 | platform |
插件,所有嵌入式對象的引用 | plugins |
是否啟用驅(qū)動 | webdriver |
引擎名 | product |
硬件支持并發(fā)數(shù) | hardwareConcurrency |
網(wǎng)絡信息 | connection |
是否啟用java | javaEnabled() |
是否啟用數(shù)據(jù)污點 | taintEnabled() |
-
Location
-
Location對象包含有關當前URL的信息
-
URL錨 | hash |
---|---|
當前主機名和端口號 | host |
當前主機名 | hostname |
當前URL | href |
當前URL的路徑 | pathname |
當前URL的端口號 | port |
當前URL的協(xié)議 | protocol |
設置URL查詢部分 | search |
加載新文件 | assign() |
重新加載文件 | reload() |
替換當前文檔 | replace() |
-
Screen
-
每個window對象的screen屬性都引用一個Screen對象。Screen對象中存放著有關顯示瀏覽器屏幕的信息。
-
屏幕高度 | avaiHeight |
---|---|
屏幕寬度 | availWidth |
調(diào)色版比特深度 | bufferDepth |
顯示屏每英寸水平點數(shù) | deviceXDPI |
顯示屏每英寸垂直點數(shù) | deviceYDPI |
是否啟用字體平滑 | fontSmoothingEnabled |
顯示屏高度 | height |
顯示屏分辨率 | pixeIDepth |
屏幕刷新率 | updateInterval |
顯示屏寬度 | width |
-
History
-
History對象包含用戶在瀏覽器窗口中訪問過的URL。
-
瀏覽器歷史列表中的URL數(shù)量 | length |
---|---|
加載前一個URL | back() |
加載下一個URL | forward() |
加載某個具體頁面 | go() |
window中還有很多屬性和方法,這里就不再過多的描述,大家可以自行查看。
1.4 事件
HTML 事件是發(fā)生在 HTML 元素上的事情。
當在 HTML 頁面中使用 JavaScript 時, JavaScript 可以觸發(fā)這些事件。
HTML 事件可以是瀏覽器行為,也可以是用戶行為。
以下是 HTML 事件的實例:
-
HTML 頁面完成加載
-
HTML input 字段改變時
-
HTML 按鈕被點擊
通常,當事件發(fā)生時,你可以做些事情。
在事件觸發(fā)時 JavaScript 可以執(zhí)行一些代碼。
HTML 元素中可以添加事件屬性,使用 JavaScript 代碼來添加 HTML 元素。
事件可以用于處理表單驗證,用戶輸入,用戶行為及瀏覽器動作:
-
頁面加載時觸發(fā)事件
-
頁面關閉時觸發(fā)事件
-
用戶點擊按鈕執(zhí)行動作
-
驗證用戶輸入內(nèi)容的合法性
-
等等 ...
可以使用多種方法來執(zhí)行 JavaScript 事件代碼:
-
HTML 事件屬性可以直接執(zhí)行 JavaScript 代碼
-
HTML 事件屬性可以調(diào)用 JavaScript 函數(shù)
-
你可以為 HTML 元素指定自己的事件處理程序
-
你可以阻止事件的發(fā)生。
-
等等 ..
HTML事件
事件 | 描述 |
---|---|
onclick | 當用戶單擊HTML元素時觸發(fā) |
ondblclick | 當用戶雙擊對象時觸發(fā) |
onmove | 當對象移動時觸發(fā) |
onmoveend | 當對象停止移動時觸發(fā) |
onmovestart | 當對象開始移動時觸發(fā) |
onkeydown | 當用戶按下鍵盤按鍵時觸發(fā) |
onkeyup | 當用戶釋放鍵盤按鍵時觸發(fā) |
onload | 當某個頁面或圖像被完成加載 |
onselect | 當文本被選定 |
onblur | 當元素失去焦點 |
onchange | 當HTML元素改變時觸發(fā) |
onfocusin | 當元素將要被設置為焦點之前觸發(fā) |
onhelp | 當用戶在按F1鍵時觸發(fā) |
onkeypress | 當用戶按下字面鍵時觸發(fā) |
onmousedown | 當用戶用任何鼠標按鈕單擊對象時觸發(fā) |
onmousemove | 當用戶將鼠標劃過對象時觸發(fā) |
onmouseover | 當用戶從一個HTML元素上移動鼠標時觸發(fā) |
onmouseout | 當用戶從一個HTML元素上移開鼠標時觸發(fā) |
onmouseup | 當用戶在對象之上釋放鼠標按鈕時觸發(fā) |
onmousewheel | 當鼠標滾輪按鈕旋轉(zhuǎn)時觸發(fā) |
onstop | 當用戶單擊停止按鈕或者離開頁面時觸發(fā) |
onactivate | 當對象設置為活動元素時觸發(fā) |
onreadystatechange | 當在對象上發(fā)生對象屬性更改時觸發(fā) |
ondragend | 當用戶拖拽操作結(jié)束后釋放鼠標時觸發(fā) |
接下來說一下HTMl中綁定事件的幾種方法,分別是行內(nèi)綁定、動態(tài)綁定、事件監(jiān)聽、bind和on綁定。
-
行內(nèi)綁定是指把觸發(fā)事件直接寫到元素的標簽中
<li>
<div onclick="xxx()">點擊</div>
</li>
-
動態(tài)綁定是指先獲取到dom元素,然后在元素上綁定事件
<script>
var xx = document.getElementById("lx");
xx.onclick = function(){}
</script>
-
事件監(jiān)聽主要通過addEventListener()方法來實現(xiàn)
<script>
var xx = document.getElementById("lx");
xx.addEventListener("click", function(){})
</script>
-
bind()和on()綁定都是屬于JQuery的事件綁定方法,bind()的事件函數(shù)只能針對已經(jīng)存在的元素進行事件的設置
$("button").bind("click",function(){
$("p").slideToggle();
});
-
on()支持對將要添加的新元素設置事件
$(document).ready(function(){
$("p").on("click", function(){});
});
還有l(wèi)ive()和delegate()等事件綁定方法,目前并不常用。
二、瀏覽器控制臺
首先介紹一下瀏覽器控制臺的使用,以開發(fā)者使用最多的chrome為例。Windows操作系統(tǒng)下的F12鍵可以打開控制臺,mac操作系統(tǒng)下用Fn+F12鍵打開。我們選擇平時使用較多的模塊進行介紹
2.1 Network
Network是Js調(diào)試的重點,面板上由控制器、過濾器、數(shù)據(jù)流概覽、請求列表、數(shù)據(jù)統(tǒng)計這五部分組成。
-
控制器:Presserve Log是保留請求日志的作用,在跳轉(zhuǎn)頁面的時候勾選上可以看到跳轉(zhuǎn)前的請求。Disable cache是禁止緩存的作用,Offline是離線模擬。
-
過濾器:根據(jù)規(guī)則過濾器請求列表的內(nèi)容,可以選擇XHR,JS,CSS,WS等。
-
數(shù)據(jù)流概覽:顯示HTTP請求、響應的時間軸。
-
請求列表:默認是按時間排序,可以看到瀏覽器所有的請求,主要用于網(wǎng)絡請求的查看和分析,可以查看請求頭、響應狀態(tài)和內(nèi)容、Form表單等。
-
數(shù)據(jù)統(tǒng)計:請求總數(shù)、總數(shù)據(jù)量、總花費時間等。
瀏覽器控制臺Network下各項屬性的含義
作用:
-
可以查看調(diào)取接口是否正確,后臺返回的數(shù)據(jù);
-
查看請求狀態(tài)、請求類型、請求地址
Network-Headers
首先打開控制臺,找到Network. 刷新頁面可以看到Name
Name對應的是資源的名稱及路徑, Status Code 是請求服務器返回的狀態(tài)碼,一般情況下當狀態(tài)碼為200時,則表示接口匹配成功。點擊任一文件名,右側(cè)會出現(xiàn)Header選項。
Network-Header-General
-
Request URL: 資源請求的url
-
Request Method: 請求方法(HTTP方法)
-
Status Code: 狀態(tài)碼
-
200(狀態(tài)碼) OK
-
301 - 資源(網(wǎng)頁等)被永久轉(zhuǎn)移到其它URL
-
404 - 請求的資源(網(wǎng)頁等)不存在
-
500 - 內(nèi)部服務器錯誤(后臺問題)
-
-
Remote Address: 遠程地址;
-
Referrer Policy: 控制請求頭中 refrrer 的內(nèi)容 包含值的情況:
-
"", 空串默認按照瀏覽器的機制設置referrer的內(nèi)容,默認情況下是和no-referrer-when-downgrade設置得一樣
-
"no-referrer", 不顯示 referrer的任何信息在請求頭中
-
"no-referrer-when-downgrade", 默認值。當從https網(wǎng)站跳轉(zhuǎn)到http網(wǎng)站或者請求其資源時(安全降級HTTPS→HTTP),不顯示 referrer的信息,其他情況(安全同級HTTPS→HTTPS,或者HTTP→HTTP)則在 referrer中顯示完整的源網(wǎng)站的URL信息
-
"same-origin", 表示瀏覽器只會顯示 referrer信息給同源網(wǎng)站,并且是完整的URL信息。所謂同源網(wǎng)站,是協(xié)議、域名、端口都相同的網(wǎng)站
-
"origin", 表示瀏覽器在 referrer字段中只顯示源網(wǎng)站的源地址(即協(xié)議、域名、端口),而不包括完整的路徑
-
"strict-origin", 該策略更為安全些,和 origin策略相似,只是不允許 referrer信息顯示在從https網(wǎng)站到http網(wǎng)站的請求中(安全降級)
-
"origin-when-cross-origin", 當發(fā)請求給同源網(wǎng)站時,瀏覽器會在 referrer中顯示完整的URL信息,發(fā)個非同源網(wǎng)站時,則只顯示源地址(協(xié)議、域名、端口)
-
"strict-origin-when-cross-origin", 和 origin-when-cross-origin相似,只是不允許 referrer信息顯示在從https網(wǎng)站到http網(wǎng)站的請求中(安全降級)
-
"unsafe-url" 瀏覽器總是會將完整的URL信息顯示在 referrer字段中,無論請求發(fā)給任何網(wǎng)站
-
補充: 什么是referrer?
-
當一個用戶點擊頁面中的一個鏈接,然后跳轉(zhuǎn)到目標頁面時,本變頁面會收到一個信息,即用戶是從哪個源鏈接跳轉(zhuǎn)過來的。
-
也就是說當你發(fā)起一個HTTP請求,請求頭中的 referrer 字段就說明了你是從哪個頁面發(fā)起該請求的;
-
-
Network-Header-Response Headers
-
Access-Control-Allow-Origin: 請求頭中允許設置的請求方法
-
Connection: 連接方式
-
content-length: 響應數(shù)據(jù)的數(shù)據(jù)長度,單位是byte
-
content-type: 客戶端發(fā)送的類型及采用的編碼方式
-
Date: 客戶端請求服務端的時間
-
Vary: 用來指示緩存代理(例如squid)根據(jù)什么條件去緩存一個請求
-
Last-Modified: 服務端對該資源最后修改的時間
-
Server: 服務端的web服務端名
-
Content-Encoding: gzip 壓縮編碼類型
-
Transfer-Encoding:chunked: 分塊傳遞數(shù)據(jù)到客戶端
Network-Header-Request Headers
-
Accept: 客戶端能接收的資源類型
-
Accept-Encoding: 客戶端能接收的壓縮數(shù)據(jù)的類型
-
Accept-Language: 客戶端接收的語言類型
-
Cache-Control: no-cache 服務端禁止客戶端緩存頁面數(shù)據(jù)
-
Connection: keep-alive 維護客戶端和服務端的連接關系
-
Cookie:客戶端暫存服務端的信息
-
Host: 連接的目標主機和端口號
-
Pragma: no-cache 服務端禁止客戶端緩存頁面數(shù)據(jù)
-
Referer: 來于哪里(即從哪個頁面跳轉(zhuǎn)過來的)
-
User-Agent: 客戶端版本號的名字
2.2 Sources
Sources按列分為三列,從左至右分別是文件列表區(qū)、當前文件區(qū)、斷點調(diào)試區(qū)。
文件列表區(qū)中有Page、Snippets、FileSytem等。Page可以看到當前所在的文件位置,在Snippets中單擊New Snippets可以添加自定義的Js代碼,F(xiàn)ileSytem可以把本地的文件系統(tǒng)導入到chrome中。
當前文件區(qū)是需要重點操作的區(qū)域,單擊下方的{}來格式化代碼,就能看到美觀的Js代碼,然后可以根據(jù)指定行數(shù)進行斷點調(diào)試。
斷點調(diào)試區(qū)也非常重要,每個操作點都需要了解是什么作用。最上方的功能區(qū)分別是暫停、跳過、進入、跳出、步驟進入、禁用斷點、異常斷點。
Watch:變量監(jiān)聽,對加入監(jiān)聽列表的變量進行監(jiān)聽。
Call Stack:斷點的調(diào)用堆棧列表,完整地顯示了導致代碼被暫停的執(zhí)行路徑。
Scope:當前斷點所在函數(shù)執(zhí)行的作用域內(nèi)容。
Breakpoints:斷點列表,將每個斷點所在文件/行數(shù)/改成簡略內(nèi)容進行展示。
DOM Breakpoints:DOM斷點列表。
XHR/fetch Breakpoints:對達到滿足過濾條件的請求進行斷點攔截。
Event Listener Breakpoints:打開可監(jiān)聽的事件監(jiān)聽列表,可以在監(jiān)聽事件并且觸發(fā)該事件時進入斷點,調(diào)試器會停留在觸發(fā)事件代碼行。
2.3 Application
Application是應用管理部分,主要記錄網(wǎng)站加載的所有資源信息。包括存儲數(shù)據(jù)(Local Storage、Session Storage、InDexedDB、Web SQL、Cookies)、緩存數(shù)據(jù)、字體、圖片、腳本、樣式表等。Local Storage(本地存儲)和 Session Storage中可以查看和管理其存儲的鍵值對。這里使用最多的是對Cookies的管理了,有時候調(diào)試需要清除Cookies,可以在Application的Cookies位置單擊鼠標右鍵,選擇Clear進行清除,或者根據(jù)Cookies中指定的Name和Value來進行清除,便于進一步調(diào)試。
注意:我們辨別Cookie來源時,可以看httpOnly這一欄,有√的是來自于服務端,沒有√的則是本地生成的。
2.4 Console
谷歌控制臺中的Console區(qū)域用于審查DOM元素、調(diào)試JavaScript代碼、查看HTML解析,一般是通過Console.log()來輸出調(diào)試信息。在Console中也可以輸出window、document、location等關鍵字查看瀏覽器環(huán)境,如果對某函數(shù)使用了斷點,也可以在Console中調(diào)用該函數(shù)。
如果你平時只是用console.log()來輸出一些變量的值,那你肯定還沒有用過console的強大的功能。下面帶你用console玩玩花式調(diào)試。
來看下主要的調(diào)試函數(shù)及用法:
console.log(), console.error(), console.warn(), console.info()
最基本也是最常用的用法了,分別表示輸出普通信息、錯誤信息、警示信息和提示性信息,且error和warn方法有特定的圖標和顏色標識。
-
console.assert(expression, message)
-
參數(shù):
-
expression: 條件語句,語句會被解析成 Boolean,且為 false 的時候會觸發(fā)message語句輸出
-
message: 輸出語句,可以是任意類型
-
該函數(shù)會在 expression 為 false 的時候,在控制臺輸出一段語句,輸出的內(nèi)容就是傳入的第二個參數(shù) message 的內(nèi)容。當我們在只需要在特定的情況下才輸出語句的時候,可以使用 console.assert
-
function greaterThan(a,b) { console.assert(a > b, {"message":"a is not greater than b","a":a,"b":b}); } greaterThan(5,6);
-
-
console.count(label)
-
參數(shù):
-
label: 計算數(shù)量的標識符
-
該函數(shù)用于計算并輸出特定標識符為參數(shù)的console.count函數(shù)被調(diào)用的次數(shù)。下面的例子更能直觀的了解:
-
function login(name) { console.count(name + ' logged in'); }
-
-
console.dir(object)
-
參數(shù):
-
object:被輸出扎實的對象
-
該函數(shù)用于打印出對象的詳細的屬性、函數(shù)及表達式等信息。如果該對象已經(jīng)被記錄為一個HTML元素,則該HTML元素的DOM表達式的屬性會被像下面這樣打印出來:
-
console.dir(document.body);
-
-
console.dirxml(object)
-
該函數(shù)將打印輸出XML元素及其子孫后代元素,且對HTML和XML元素調(diào)用 console.dirxml() 和 調(diào)用 console.log() 是等價的
-
-
console.group([label]), console.groupEnd([label])
-
參數(shù):
-
label: group分組的標識符
-
在控制臺創(chuàng)建一個新的分組,隨后輸出到控制臺上的內(nèi)容都會自動添加一個縮進,表示該內(nèi)容屬于當前分組,知道調(diào)用 console.groupEnd() 之后,當前分組結(jié)束。
-
舉個例子:
-
console.log("This is the outer level"); console.group(); console.log("Level 2"); console.group(); console.log("Level 3"); console.warn("More of level 3"); console.groupEnd(); console.log("Back to level 2"); console.groupEnd(); console.log("Back to the outer level");
-
-
console.groupCollapsed(label)
-
該函數(shù)同console.group(),唯一的區(qū)別是該函數(shù)的輸出默認不展開分組,而console.group()是默認展開分組。
-
-
console.time([label]), console.timeEnd([label])
-
label: 用于標記計時器的名稱,不填的話,默認為 default
-
console.time() 會開始一個計時器,并當執(zhí)行到 console.timeEnd() 函數(shù)時(需要兩個函數(shù)的lable參數(shù)相同),結(jié)束計時器,并將計時器的總時間輸出到控制臺上。
-
再舉幾個例子:
-
console.time(); var arr = new Array(10000); for (var i = 0; i < arr.length; i++) { arr[i] = new Object(); } console.timeEnd(); // default: 3.696044921875ms
-
對 console.time(label) 設置一個自定義的 label 字段,并使用console.timeEnd(label) 設置相同的 label 字段來結(jié)束計時器。
-
console.time('total'); var arr = new Array(10000); for (var i = 0; i < arr.length; i++) { arr[i] = new Object(); } console.timeEnd('total'); // total: 3.696044921875ms
-
設置多個 label 屬性,開啟多個計時器同步計時。
-
console.time('total'); console.time('init arr'); var arr = new Array(10000); console.timeEnd('init arr'); for (var i = 0; i < arr.length; i++) { arr[i] = new Object(); } console.timeEnd('total'); // init arr: 0.0546875ms // total: 2.5419921875ms
-
-
console.trace(object)
-
該函數(shù)將在控制臺打印出從 console.trace() 被調(diào)用的位置開始的堆棧信息。
-
三、加密參數(shù)的定位方法
想要找到Js加密參數(shù)的生成過程,就必須要找到參數(shù)的位置,然后通過debug來進行觀察調(diào)試。我們總結(jié)了目前通用的調(diào)試方式。每種方法都有其獨特的運用之道,大家只有靈活運用這些參數(shù)定位方法,才能更好地提高逆向效率。
3.1 巧用搜索
搜索操作比較簡單,打開控制臺,通過快捷鍵Ctrl + F打開搜索框。在Network中的不同位置使用Ctrl + F會打開不同的搜索區(qū)域,有全局搜索、頁面搜索。
另外關于搜索也有一定的技巧,如果加密參數(shù)的關鍵詞是signature,可以直接全局搜索signature,搜索不到可以嘗試搜索sign或者搜索接口名。如果還沒有找到位置,則可以使用下面幾種方法。
3.2 堆棧調(diào)試
控制臺的 Initiator 堆棧調(diào)試是我們比較喜歡的調(diào)試方式之一,不過新版本的谷歌瀏覽器才有,如果沒有 Initiator 需要更新Chrome版本。Initiator主要是為了監(jiān)聽請求是怎樣發(fā)起的,通過它可以快速定位到調(diào)用棧中。
具體使用方法是先確定請求的接口,然后進入Initiator,單擊第一個Request call stack參數(shù),進入Js文件后,在跳轉(zhuǎn)行上打上斷點,然后刷新頁面等待調(diào)試。
3.3 控制臺調(diào)試
控制臺的Console中可以由console.log()方法來執(zhí)行某些函數(shù),該方法對于開發(fā)調(diào)試很有幫助,有時通過輸出會比找起來更便捷。在斷點到某一處時,可以通過console.log()輸出此時的參數(shù)來查看狀態(tài)和屬性,console.log()方法在后面的參數(shù)還原中也很重要。
3.4 監(jiān)聽XHR
XHR是XMLHttpRequest的簡稱,通過監(jiān)聽XHR的斷點,可以匹配URl中params參數(shù)的觸發(fā)點和調(diào)用堆棧,另外post請求中From Data的參數(shù)也可以用XHR來攔截。
使用方法:打開控制臺,單擊Sources,右側(cè)有一個XHR/fetch Breakpoints,單擊+號即可添加監(jiān)聽事件。像一些URL中的_signature參數(shù)就很適合使用XHR斷點。
3.5 事件監(jiān)聽
這里其實和監(jiān)聽XHR有些相似,為了方便記憶,我們將其單獨放在一個小節(jié)中。
有的時候找不到參數(shù)位置,但是知道它的觸發(fā)條件,此時可以使用事件監(jiān)聽器進行斷點,在Sources中有
DOM Breakpoints、Global Listeners、Event Listener Breakpoints都可以進行DOM事件監(jiān)聽。
比如需要對Canvas進行斷點,就在Event Listener Breakpoints中選擇Canvas,勾選Create canvas context時就是對創(chuàng)建canvas時的事件進行了斷點。
3.6 添加代碼片
在控制臺中添加代碼片來完成Js代碼注入,也是一種不錯的方式。
使用方法:打開控制臺,單擊Sources,然后單擊左側(cè)的snippets,新建一個Script Snippet,就可以在空白區(qū)域編輯Js代碼了。
3.7 Hook
在Js中也需要用到Hook技術,例如當想分析某個cookie是如何生成時,如果想通過直接從代碼里搜索該cookie的名稱來找到生成邏輯,可能會需要審核非常多的代碼。這個時候,如果能夠用hook document.cookie的set方法,那么就可以通過打印當時的調(diào)用方法堆?;蛘咧苯酉聰帱c來定位到該cookie的生成代碼位置。
什么是hook?
在 JS 逆向中,我們通常把替換原函數(shù)的過程都稱為 Hook。一般使用 Object.defineProperty() 來進行hook。
以下先用一段簡單的代碼理解Hook的過程:
function a() {
console.log("I'm a.");
}
?
a = function b() {
console.log("I'm b.");
};
?
a() // I'm b.
直接覆蓋原函數(shù)是以最簡單的做法,以上代碼將a函數(shù)進行了重寫,再次調(diào)用a函數(shù)將會輸出I'm b.
如果還想執(zhí)行原來a函數(shù)的內(nèi)容,可以使用中間變量進行存儲:
function a() {
console.log("I'm a.");
}
?
var c = a;
?
a = function b() {
console.log("I'm b.");
};
?
a() // I'm b.
c() // I'm a.
?
此時,調(diào)用 a 函數(shù)會輸出 I’m b.,調(diào)用 c 函數(shù)會輸出 I’m a.
這種原函數(shù)直接覆蓋的方法通常只用來進行臨時調(diào)試,實用性不大,但是它能夠幫助我們理解 Hook 的過程,在實際 JS 逆向過程中,我們會用到更加高級一點的方法,比如 Object.defineProperty()。
Object.defineProperty()
Object.defineProperty(obj, prop, descriptor)
-
obj:需要定義屬性的當前對象;
-
prop:當前需要定義的屬性名;
-
descriptor:屬性描述符,可以取以下值;
屬性描述符的取值通常為以下:
屬性名 | 默認值 | 含義 |
---|---|---|
get | undefined | 存取描述符,目標屬性獲取值的方法 |
set | undefined | 存取描述符,目標屬性設置值的方法 |
value | undefined | 數(shù)據(jù)描述符,設置屬性的值 |
writable | false | 數(shù)據(jù)描述符,目標屬性的值是否可以被重寫 |
enumerable | false | 目標屬性是否可以被枚舉 |
configurable | false | 目標屬性是否可以被刪除或是否可以再次修改特性 |
通常情況下,對象的定義與賦值是這樣的:
var people = {}
people.name = "Bob"
people["age"] = "18"
?
console.log(people)
// { name: 'Bob', age: '18' }
使用 defineProperty() 方法:
var people = {}
?
Object.defineProperty(people, 'name', {
value: 'Bob',
writable: true // 是否可以被重寫
})
?
console.log(people.name) // 'Bob'
?
people.name = "Tom"
console.log(people.name) // 'Tom'
?
我們一般hook使用的是get和set方法:
var people = {
name: 'Bob',
};
var count = 18;
?
// 定義一個 age 獲取值時返回定義好的變量 count
Object.defineProperty(people, 'age', {
get: function () {
console.log('獲取值!');
return count;
},
set: function (val) {
console.log('設置值!');
count = val + 1;
},
});
?
console.log(people.age);
people.age = 20;
console.log(people.age);
輸出:
獲取值!
18
設置值!
獲取值!
21
通過這樣的方法,我們就可以在設置某個值的時候,添加一些代碼,比如 debugger;
讓其斷下,然后利用調(diào)用棧進行調(diào)試,找到參數(shù)加密、或者參數(shù)生成的地方,需要注意的是,網(wǎng)站加載時首先要運行我們的Hook代碼,再運行網(wǎng)站自己的代碼,才能夠成功斷下,這個過程我們可以稱之為Hook代碼的注入。
TamperMonkey 注入
TamperMonkey 俗稱油猴插件,是一款免費的瀏覽器擴展和最為流行的用戶腳本管理器,支持很多主流的瀏覽器, 包括 Chrome、Microsoft Edge、Safari、Opera、Firefox、UC 瀏覽器、360 瀏覽器、QQ 瀏覽器等等,基本上實現(xiàn)了腳本的一次編寫,所有平臺都能運行,可以說是基于瀏覽器的應用算是真正的跨平臺了。用戶可以在 GreasyFork、OpenUserJS 等平臺直接獲取別人發(fā)布的腳本,功能眾多且強大,比如視頻解析、去廣告等。
來到某奇藝首頁,可以看到cookie里面有個__dfp值:
我們想通過Hook的方式,讓在生成__dfp的地方斷下,就可以編寫如下函數(shù):
我們以某奇藝的 cookie 為例來演示如何編寫 TamperMonkey 腳本,首先去應用商店安裝 TamperMonkey,安裝過程不再贅述,然后點擊圖標,添加新腳本,或者點擊管理面板,再點擊加號新建腳本,寫入以下代碼:
(function () {
'use strict';
var cookieTemp = '';
Object.defineProperty(document, 'cookie', {
set: function (val) {
if (val.indexOf('__dfp') != -1) {
debugger;
}
console.log('Hook捕獲到cookie設置->', val);
cookieTemp = val;
return val;
},
get: function () {
return cookieTemp;
},
});
})();
if (val.indexOf('__dfp') != -1) {debugger;}
的意思是檢索 __dfp
在字符串中首次出現(xiàn)的位置,等于 -1 表示這個字符串值沒有出現(xiàn),反之則出現(xiàn)。如果出現(xiàn)了,那么就 debugger 斷下,這里要注意的是不能寫成 if (val == '__dfp') {debugger}
,因為 val 傳過來的值類似于 __dfp=xxxxxxxxxx
,這樣寫是無法斷下的。
寫入后進行保存
主體的JavaScript自執(zhí)行函數(shù)和前面的都是一樣的,這里需要注意的是最前面的注釋,每個選項都是有意義的,所有的選項參考 TamperMonkey 官方文檔,以下列出了比較常用、比較重要的部分選項(其中需要特別注意 @match、@include、@run-at)
選項 | 含義 |
---|---|
@name | 腳本的名稱 |
@namespace | 命名空間,用來區(qū)分相同名稱的腳本,一般寫作者名字或者網(wǎng)址就可以 |
@version | 腳本版本,油猴腳本的更新會讀取這個版本號 |
@description | 描述這個腳本是干什么用的 |
@author | 編寫這個腳本的作者的名字 |
@match | 從字符串的起始位置匹配正則表達式,只有匹配的網(wǎng)址才會執(zhí)行對應的腳本,例如 * 匹配所有,https://www.baidu.com/* 匹配百度等,可以參考 Python re 模塊里面的 re.match() 方法,允許多個實例 |
@include | 和 @match 類似,只有匹配的網(wǎng)址才會執(zhí)行對應的腳本,但是 @include 不會從字符串起始位置匹配,例如 *://*baidu.com/* 匹配百度,具體區(qū)別可以參考 TamperMonkey 官方文檔
|
@icon | 腳本的 icon 圖標 |
@grant | 指定腳本運行所需權限,如果腳本擁有相應的權限,就可以調(diào)用油猴擴展提供的 API 與瀏覽器進行交互。如果設置為 none 的話,則不使用沙箱環(huán)境,腳本會直接運行在網(wǎng)頁的環(huán)境中,這時候無法使用大部分油猴擴展的 API。如果不指定的話,油猴會默認添加幾個最常用的 API |
@require | 如果腳本依賴其他 JS 庫的話,可以使用 require 指令導入,在運行腳本之前先加載其它 |
@run-at | 腳本注入時機,該選項是能不能 hook 到的關鍵,有五個值可選:document-start :網(wǎng)頁開始時;document-body :body出現(xiàn)時;document-end :載入時或者之后執(zhí)行;document-idle :載入完成后執(zhí)行,默認選項;context-menu :在瀏覽器上下文菜單中單擊該腳本時,一般將其設置為 document-start
|
清除 cookie,開啟 TamperMonkey 插件,再次來到某奇藝首頁,可以成功被斷下,也可以跟進調(diào)用棧來進一步分析 __dfp 值的來源。
四、常見的壓縮和混淆
在Web系統(tǒng)發(fā)展早期,Js在Web系統(tǒng)中承擔的職責并不多,Js文件比較簡單,也不需要任何的保護。隨著Js文件體積的增大和前后端交互增多,為了加快http傳輸速度并提高接口的安全性,出現(xiàn)了很多的壓縮工具和混淆加密工具。
代碼混淆的本質(zhì)是對于代碼標識和結(jié)構的調(diào)整,從而達到不可讀不可調(diào)用的目的,常用的混淆有字符串、變量名混淆,比如把字符串轉(zhuǎn)換成_0x,把變量重命名等,從結(jié)構的混淆包括控制流平坦化,虛假控制流和指令替換,代碼加密主要有通過veal方法去執(zhí)行字符串函數(shù),通過escape()等方法編碼字符串、通過轉(zhuǎn)義字符加密代碼、自定義加解密方法(RSA、Base64、AES、MD5等),或者通過一些開源的工具進行加密。
另外目前市面上比較常見的混淆還有ob混淆(obfuscator),特征是定義數(shù)組,數(shù)組位移。不僅Js中的變量名混淆,運行邏輯等也高度混淆,應對這種混淆可以使用已有的工具ob-decrypt或者AST解混淆或者使用第三方提供的反混淆接口。大家平時可以多準備一些工具,在遇到無法識別的Js時,可以直接使用工具來反混淆和解密,當然逆向工作本身就很看運氣。
-
例如翻看網(wǎng)站的Javascript源代碼,可以發(fā)現(xiàn)很多壓縮或者看不太懂的字符,如javascript文件名被編碼,文件的內(nèi)容被壓縮成幾行,變量被修改成單個字符或者一些十六進制的字符——這些導致我們無法輕易根據(jù)Javascript源代碼找出某些接口的加密邏輯。
4.1 JavaScript壓縮
這個非常簡單,Javascript壓縮即去除JavaScript代碼中不必要的空格、換行等內(nèi)容或者把一些可能公用的代碼進行處理實現(xiàn)共享,最后輸出的結(jié)果都壓縮為幾行內(nèi)容,代碼的可讀性變得很差,同時也能提高網(wǎng)站的加載速度。
如果僅僅是去除空格、換行這樣的壓縮方式,其實幾乎是沒有任何防護作用的,因為這種壓縮方式僅僅是降低了代碼的直接可讀性。因為我們有一些格式化工具可以輕松將JavaScirpt代碼變得易讀,比如利用IDE、在線工具或Chrome瀏覽器都能還原格式化的代碼。
這里舉一個最簡單的JavaScript壓縮示例。原來的JavaScript代碼是這樣的:
function echo(stringA, stringB){
const name = "Germey";
alert("hello " + name);
}
壓縮之后就變成這樣子:
function echo(d,c){const e="Germey";alert("hello "+e)};
可以看到,這里參數(shù)的名稱都被簡化了,代碼中的空格也被去掉了,整個代碼也被壓縮成了一行,代碼的整體可讀性降低了。
目前主流的前端開發(fā)技術大多都會利用webpack、Rollup等工具進行打包。webpack、Rollup會對源代碼進行編譯和壓縮,輸出幾個打包好的JavaScript文件,其中我們可以看到輸出的JavaScript文件名帶有一些不規(guī)則的字符串,同時文件內(nèi)容可能只有幾行,變量名都用一些簡單字母表示。這其中就包含JavaScript壓縮技術,比如一些公共的庫輸出成bundle文件,一些調(diào)用邏輯壓縮和轉(zhuǎn)義成冗長的幾行代碼,這些都屬于JavaScript壓縮,另外,其中也包含了一些很基礎的JavaScript混淆技術,比如把變量名、方法名替換成一些簡單的字符,降低代碼的可讀性。
但整體來說,JavaScript壓縮技術只能在很小的程度上起到防護作用,想要真正的提高防護效果,還得依靠JavaScript混淆和加密技術。
4.2 JavaScript混淆
JavaScript混淆完全是在JavaScript上面進行的處理,它的目的就是使得JavaScript變得難以閱讀和分析,大大降低代碼的可讀性,是一種很實用的JavaScript保護方案。
JavaScript混淆技術主要有一下幾種。
-
變量名混淆:將帶有含義的變量名、方法名、常量名隨機變?yōu)闊o意義的類亂碼字符串,降低代碼的可讀性,如轉(zhuǎn)換成單個字符或十六進制字符串。
-
字符串混淆:將字符串陣列化集中并可進行MD5或Base64加密存儲,使代碼中不出現(xiàn)明文字符串,這樣可以避免使用全局搜索字符串的方式定位到入口。
-
對象鍵名替換:針對JavaScript對象的屬性進行加密轉(zhuǎn)化,隱藏代碼之間的調(diào)用關系。
-
控制流平坦化:打亂函數(shù)原有代碼的執(zhí)行流程及函數(shù)調(diào)用關系,使代碼邏輯變得混亂無序。
-
無用代碼注入:隨機在代碼中插入不會被執(zhí)行到的無用代碼,進一步使代碼看起來更加混亂。
-
調(diào)試保護:基于調(diào)試器特征,對當前運行環(huán)境進行檢查,加入一些debugger語句,使其在調(diào)試模式下難以順利執(zhí)行JavaScript代碼。
-
多態(tài)變異:使JavaScript代碼每次被調(diào)用時,將代碼自身立刻自動發(fā)生變異,變?yōu)榕c之前完全不同的代碼,即功能完全不變,只是代碼形式變異,以此杜絕代碼被動態(tài)分析和調(diào)試。
-
域名鎖定:使JavaScript代碼只能在指定域名下執(zhí)行。
-
代碼自我保護:如果對JavaScript代碼進行格式化,則無法執(zhí)行,導致瀏覽器假死。
-
特殊編碼:將JavaScript完全編碼為人不可讀的代碼,如表情符號、特殊表示內(nèi)容、等等。
總之,以上方案都是JavaScript混淆的實現(xiàn)方式,可以在不同程度上保護JavaScript代碼。
在前端開發(fā)中,現(xiàn)在JavaScript混淆的主流實現(xiàn)使javascript-obfuscator和terser這兩個庫。它們都能提供一些代碼混淆功能,也都有對應的webpack和Rollup打包工具的插件。利用它們,我們可以非常方便地實現(xiàn)頁面的混淆,最終輸出壓縮和混淆后的JavaScript代碼,使得JavaScript代碼的可讀性大大降低。
下面我們以javascript-obfuscator為例來介紹一些代碼混淆的實現(xiàn),了解了實現(xiàn),那么我們自然就對混淆的機制有了更加深刻的認識。
javascript-obfuscator的官方介紹內(nèi)容如下:
鏈接:Javascript Obfuscator - Protects JavaScript code from stealing and shrinks size - 100% Free
它是支持ES8的免費、高效的JavaScript混淆庫,可以使得JavaScript代碼經(jīng)過混淆后難以被復制、盜用、混淆后的代碼具有和原來的代碼一模一樣的功能。
首先我們需要安裝好Node.js 12.x及以上版本,確??梢哉J褂胣pm命令。
具體的安裝方式如下:
簡單的說 Node.js 就是運行在服務端的 JavaScript。
Node.js 是一個基于 Chrome JavaScript 運行時建立的一個平臺。
Node.js 是一個事件驅(qū)動 I/O 服務端 JavaScript 環(huán)境,基于 Google 的 V8 引擎,V8 引擎執(zhí)行 Javascript 的速度非???,性能非常好。
-
官網(wǎng):Node.js
-
文檔:Index | Node.js v21.6.2 Documentation
-
中文網(wǎng):Node.js 中文網(wǎng)
-
基礎教程:Node.js 教程 | 菜鳥教程
接著新建一個文件夾,比如js-ob然后進入該文件夾,初始化工作空間:
npm init
這里會提示我們輸入一些信息,然后創(chuàng)建package.json文件,這就完成了項目的初始化了。
接下來,我們來安裝javascript-obfuscator這個庫:
npm i -D javascript-obfuscator
稍等片刻,即可看到本地js-ob文件下生成了一個node_modules文件夾,里面就包含了javascript-obfuscator這個庫,這就說明安裝成功了。
接下來,我們就可以編寫代碼來實現(xiàn)一個混淆樣例了。比如,新建main.js文件,其內(nèi)容如下:
const code = `
let x = '1' + 1
console.log('x', x)
`
?
const options = {
compact: false,
}
?
const obfuscator = require('javascript-obfuscator')
?
function obfuscate(code, options) {
return obfuscator.obfuscate(code, options).getObfuscatedCode()
}
?
console.log(obfuscate(code, options))
這里我們定義了兩個變量:一個是code,即需要被混淆的代碼;另一個是混淆選項options,是一個Object。接下來,我們引入了javascript-obfuscator這個庫,然后定義了一個方法,給其傳入code和options來獲取混淆之后的代碼,最后控制臺輸出混淆后的代碼。
代碼邏輯比較簡單,我們來執(zhí)行一下代碼:
node main.js
輸出結(jié)果如下:
看到了吧,那么簡單的代碼,被我們混淆成了這個樣子,其實這里我們就是設定了“控制流平坦化”選項。整體看來,代碼的可讀性大大降低了,JavaScript調(diào)試的難度也大大加強了。
4.3 javascript-obfuscator示例
下面的例子代碼同上
4.3.1 代碼壓縮
這里javascript-obfuscator也提供了代碼壓縮的功能,使用其參數(shù)compact即可完成JavaScript代碼的壓縮,輸出為一行內(nèi)容。參數(shù)compact的默認值是true,如果定義為false,則混淆后的代碼會分行顯示。
如果不設置或者把compact設置為true,結(jié)果如下:
可以看到,單行顯示的時候,對變量名進行了進一步的混淆,這里變量的命名都變成了十六進制形式的字符串,這是因為啟用了一些默認壓縮和混淆的方式??傊覀兛梢钥吹酱a的可讀性相比之前大大降低。
4.3.2 變量名混淆
變量名混淆可以通過在javascript-obfuscator中配置identifierNamesGenerator參數(shù)來實現(xiàn)。我們通過這個參數(shù)可以控制變量名混淆的方式,如將其設置為hexadecimal,則會將變量名替換為十六進制形式的字符串。該參數(shù)的取值如下。
-
hexadecimal:將變量名替換為十六進制形式的字符串,如0xabc123。
-
mangled:將變量名替換為普通的簡寫字符,如a,b,c等。
該參數(shù)的默認值為:hexadecimal
我們將該參數(shù)改為:mangled 來試一下:
const code = `
let x = '1' + 1
console.log('x', x)
`
?
const options = {
compact: true,
identifierNamesGenerator: 'mangled'
?
}
?
const obfuscator = require('javascript-obfuscator')
?
function obfuscate(code, options) {
return obfuscator.obfuscate(code, options).getObfuscatedCode()
}
?
console.log(obfuscate(code, options))
運行結(jié)果如下:
另外,我們還可以通過設置identifiersPrefix參數(shù)來控制混淆后的變量前綴,示例如下:
const code = `
let x = '1' + 1
console.log('x', x)
`
?
const options = {
identifiersPrefix: 'kk',
}
?
const obfuscator = require('javascript-obfuscator')
?
function obfuscate(code, options) {
return obfuscator.obfuscate(code, options).getObfuscatedCode()
}
?
console.log(obfuscate(code, options))
運行結(jié)果如下:
可以看到,混淆后的變量前綴加上了我們自定義的字符串kk。
另外,renameGlobals這個參數(shù)還可以指定是否混淆全局變量和函數(shù)名稱,默認值為false。示例如下:
const code = `
var $ = function(id){
return document.getElementById(id);
};
`
?
const options = {
renameGlobals: true,
}
?
const obfuscator = require('javascript-obfuscator')
?
function obfuscate(code, options) {
return obfuscator.obfuscate(code, options).getObfuscatedCode()
}
?
console.log(obfuscate(code, options))
運行結(jié)果如下:
可以看到,這里我們聲明了一個全局變量$,在renameGlobals設置為true之后,這個變量也被替換了。如果后文用到了這個變量,可能就會有找不到定義的錯誤,因此這個參數(shù)可能導致代碼執(zhí)行不通。
如果我們不設置 renameGlobals 或者將其設置為false,結(jié)果如下:
可以看到,最后還是有$的聲明,其全局名稱沒有被改變。
4.3.3 字符串混淆
字符串混淆,就是將一個字符串聲明放到一個數(shù)組里面,使之無法被直接搜索到。這可以通過stringArray參數(shù)來控制,默認為true。
此外,我們還可以通過rotateStringArray參數(shù)來控制數(shù)組化后結(jié)果的元素順序,默認為true。
示例如下:
const code = `
var a = 'helloworld'
`
?
const options = {
stringArray: true,
rotateStringArray: true,
}
?
const obfuscator = require('javascript-obfuscator')
?
function obfuscate(code, options) {
return obfuscator.obfuscate(code, options).getObfuscatedCode()
}
?
console.log(obfuscate(code, options))
運行結(jié)果如下:
另外,我們還可以使用unicodeEscapeSequence這個參數(shù)對字符串進行Unicode轉(zhuǎn)碼,使之更加難以辨認,示例如下:
const code = `
var a = 'hello world'
`
?
const options = {
compact: false,
unicodeEscapeSequence: true
}
?
const obfuscator = require('javascript-obfuscator')
?
function obfuscate(code, options) {
return obfuscator.obfuscate(code, options).getObfuscatedCode()
}
?
console.log(obfuscate(code, options)
可以看到,這里字符串被數(shù)字化和Unicode化,非常難以辨認。
在很多JavaScript逆向過程中,一些關鍵的字符串可能會作為切入點來查找加密入口,用了這種混淆之后,如果有人想通過全局搜索的方式搜索hello這樣的字符串找加密入口,也就沒法搜到了。
4.3.4 代碼自我保護
我們可以通過設置selfDefending參數(shù)來開啟代碼自我保護功能。開啟之后混淆后的JavaScript會強制以一行顯示。如果我們將混淆后的代碼進行格式化或者重命名,該段代碼將無法執(zhí)行。
示例如下:
const code = `
console.log('hello world')
`
?
const options = {
selfDefending: true
}
?
const obfuscator = require('javascript-obfuscator')
?
function obfuscate(code, options) {
return obfuscator.obfuscate(code, options).getObfuscatedCode()
}
?
console.log(obfuscate(code, options))
如果我們將上述代碼放到控制臺,它的執(zhí)行結(jié)果和之前是一模一樣的,沒有任何問題。
如果我們將其進行格式化,然后粘貼到瀏覽器控制臺里面,瀏覽器會直接卡死無法運行。這樣如果有人對代碼進行了格式化,就無法正常對代碼進行運行和調(diào)試,從而起到了保護作用。
4.3.5 控制流平坦化
控制流平坦化其實就是將代碼的執(zhí)行邏輯混淆,使其變得復雜、難度。其基本的思想是將一些邏輯處理塊都統(tǒng)一加上一個前驅(qū)邏輯塊,每個邏輯塊都由前驅(qū)邏輯塊進行條件判斷個分發(fā),構成一個個閉環(huán)邏輯,這導致整個執(zhí)行邏輯十分復雜、難度。
比如說這里有一段示例代碼:
console.log(c);
console.log(a);
console.log(b);
代碼邏輯一目了然,一次在控制臺輸出了c, a, b三個變量的值。但是如果把這段代碼進行控制流平坦化處理,代碼就會變成這樣:
const code = `
console.log(c);
console.log(a);
console.log(b);
`
?
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))
使用控制流平坦化可以使得執(zhí)行邏輯更加復雜、難讀,目前非常多的前端混淆都會加上這個選項。但啟用控制流平坦化之后,代碼的執(zhí)行時間會變長。
4.3.6 無用代碼注入
無用代碼即不會被執(zhí)行的代碼或?qū)ι舷挛臎]有任何影響的代碼,注入之后可以對現(xiàn)有的JavaScript代碼的閱讀形成干擾。我們可以使用deadCodeInjection參數(shù)開啟這個選項,其默認值為false。
示例:
const code = `
console.log(c);
console.log(a);
console.log(b);
`
?
const options = {
compact: false,
deadCodeInjection: true
}
?
const obfuscator = require('javascript-obfuscator')
?
function obfuscate(code, options) {
return obfuscator.obfuscate(code, options).getObfuscatedCode()
}
?
console.log(obfuscate(code, options))
這種混淆方式通過混入一些特殊的判斷條件并加入一些不會被執(zhí)行的代碼,可以對代碼起到一定的干擾作用。
4.3.7 對象鍵名替換
如果是一個對象,可以使用transformObjectKeys來對對象的鍵值進行替換,示例如下:
const code = `
(function(){
var object = {
foo: 'test1',
bar: {
baz: 'test2'
}
};
})();
`
?
const options = {
compact: false,
transformObjectKeys: true
}
?
const obfuscator = require('javascript-obfuscator')
?
function obfuscate(code, options) {
return obfuscator.obfuscate(code, options).getObfuscatedCode()
}
可以看到,Object的變量名被替換為了特殊的變量,代碼的可讀性變差,這樣我們就不好直接通過變量名進行搜尋了,這也可以起到一定的防護作用。
4.3.8 禁用控制臺輸出
我們可以使用disableConsoleOutput來禁用掉console.log輸出功能,加大調(diào)試難度,示例如下:
const code = `
console.log('hello world')
`
?
const options = {
disableConsoleOutput: true
}
?
const obfuscator = require('javascript-obfuscator')
?
function obfuscate(code, options) {
return obfuscator.obfuscate(code, options).getObfuscatedCode()
}
?
console.log(obfuscate(code, options))
此時,我們?nèi)绻麍?zhí)行這段代碼,發(fā)現(xiàn)是沒有任何輸出的,這里實際上就是將console的一些功能禁用了。
4.3.9 調(diào)試保護
我們知道,如果Javascript代碼中加入關鍵字debugger關鍵字,那么執(zhí)行到該位置的時候,就會進入斷點調(diào)試模式。如果在代碼多個位置都加入debugger關鍵字,或者定義某個邏輯來反復執(zhí)行debugger,就會不斷進入斷點調(diào)試模式,原本的代碼就無法順暢執(zhí)行了。這個過程可以稱為調(diào)試保護,即通過反復執(zhí)行debugger來使得原來的代碼無法順暢執(zhí)行。
其效果類似于執(zhí)行了如下代碼:
setInterval(() => {debugger;}, 3000)
如果我們把這段代碼粘貼到控制臺,它就會反復執(zhí)行debugger語句,進入斷點調(diào)試模式,從而干擾正常的調(diào)試流程。
在javascript-obfuscator中,我們可以使用debugProtection來啟用調(diào)試保護機制,還可以使用debugProtectionInterval來啟用無限調(diào)試(debug),使得代碼在調(diào)試過程中不斷進入斷點模式,無法順暢執(zhí)行。配置如下:
const options = {
debugProtection: true,
}
混淆后的代碼會跳到debugger代碼的位置,使得整個代碼無法順暢執(zhí)行,對JavaScript代碼的調(diào)試形成干擾。
4.3.10 域名鎖定
我們還可以通過控制domainLock來控制JavaScript代碼只能在特定域名下運行,這樣就可以降低代碼被模擬或者盜用的風險。
示例如下:
const code = `
console.log('hello world')
`
?
const options = {
domainLock: ['kk.com']
}
?
const obfuscator = require('javascript-obfuscator')
?
function obfuscate(code, options) {
return obfuscator.obfuscate(code, options).getObfuscatedCode()
}
?
console.log(obfuscate(code, options))
這里我們使用domainLock指定了一個域名kk.com,也就是設置了一個域名白名單,混淆后的代碼結(jié)果如下:
這段代碼就只能在指定的域名kk.com下運行,不能在其他網(wǎng)站運行。這樣的話,如果一些相關JavaScript代碼被單獨剝離出來,想在其他網(wǎng)站運行或者使用程序模擬運行的話,運行結(jié)果只有失敗,這樣就可以有效降低代碼被模擬或盜用的風險。
4.3.11特殊編碼
另外,還有一些特殊的工具包(jjencode,aaencode等)他們可以對代碼進行混淆和編碼。
鏈接:JSON在線 | JSON解析格式化—SO JSON在線工具
示例如下:
var kk = 100
使用jjencode工具的結(jié)果:
使用aaencode工具的結(jié)果:
可以看到,通過這些工具,原本非常簡單的代碼被轉(zhuǎn)化為一些幾乎完全不可讀的代碼,但實際上運行效果還是相同的。這些混淆方式比較另類,看起來雖然沒有什么頭緒,但實際上找到規(guī)律是非常好還原的,并沒有真正達到強力混淆的效果。
關于這種混淆代碼的解碼方法,一般直接復制到控制臺運行或者用解碼工具進行轉(zhuǎn)換,如果運行失敗,就需要按分號分割語句,逐行調(diào)試分析源碼。
以上便是對JavaScript混淆方式的介紹和總結(jié)。總的來說經(jīng)過混淆的JavaScript代碼的可讀性大大降低,同時其防護效果也大大增強。
五、常見的編碼和加密
我們在爬取網(wǎng)站的時候,會遇到一些需要分析接口或URL信息的情況,這時會有各種各樣的類似加密的情形。
-
某個網(wǎng)站的URL帶有一些看不太懂的長串加密參數(shù),要抓取就得必須懂得這些參數(shù)是怎么構造的,否則我們連完整的URL都構造不出來,更不用說爬取了。
-
在分析某個網(wǎng)站的Ajax接口時,可以看到接口的一些參數(shù)也是加密的,Request Headers 里面也可能帶有一些加密參數(shù),如果不知道這些參數(shù)的具體構造邏輯,就沒法直接用程序來模擬這些Ajax請求。
常見的編碼有base64、unicode、urlencode編碼,加密有MD5、SHA1、HMAC、DES、RSA等。
簡單介紹一下常見的編碼加密,同時附上Python實現(xiàn)加密的方法。
5.1 base64
base64是一種基于64個可打印ASCLL字符對任意字節(jié)數(shù)據(jù)進行編碼的算法,其在編碼后具有一定意義的加密作用。在逆向過程中經(jīng)常會碰到base64編碼(不論是Js逆向還是安卓逆向)。
瀏覽器提供了原生的base64編碼、解碼方法,方法名就是btoa和atob如下圖所示:
在python中使用base64:
import base64
?
print(base64.b64encode('msb'.encode()))
print(base64.b64decode('bXNi'.encode()))var str1 = "msb";
unicode和urlencode比較簡單,unicode是計算機中字符集、編碼的一項業(yè)界標準,被稱為統(tǒng)一碼、萬國碼,表現(xiàn)形式一般以“\u" 或者 "&#"開頭。urlencode是URL編碼,也稱作百分號編碼用于把URL中的符號進行轉(zhuǎn)換。
5.2 MD5
MD5消息摘要算法(英文:MD5 Message-Digest Algorithm),一種被廣泛使用的密碼散列函數(shù),可以產(chǎn)出一個128位(16字節(jié))的散列值(hash value),用于確保信息傳輸完整一致。MD5加密算法是不可逆的,所以解密一般都是通過暴力窮舉方法,以及網(wǎng)站的接口實現(xiàn)解密。
python代碼實現(xiàn)加密:
import hashlib
pwd = "123"
# 生成MD5對象
m = hashlib.md5()
# 對數(shù)據(jù)進行加密
m.update(pwd.encode('utf-8'))
# 獲取密文
pwd = m.hexdigest()
print(pwd)
解密工具:md5在線解密破解,md5解密加密
5.3 SHA1
SHA1(Secure Hash Algorithm)安全哈希算法主要適用于數(shù)字簽名標準里面定義的數(shù)字簽名算法,SHA1比MD5的安全性更強。對于長度小于2^64位的消息,SHA1會產(chǎn)生一個160位的消息摘要。
一般在沒有高度混淆的Js代碼中,SHA1加密的關鍵詞就是sha1。
Python實現(xiàn)代碼:
import hashlib
sha1 = hashlib.sha1()
data1 = "msb"
data2 = "kkk"
sha1.update(data1.encode("utf-8"))
sha1_data1 = sha1.hexdigest()
print(sha1_data1)
sha1.update(data2.encode("utf-8"))
sha1_data2 = sha1.hexdigest()
print(sha1_data2)
運行結(jié)果:
解密工具:哈希在線加密|MD5在線解密加密|SHA1在線解密加密|SHA256在線解密加密|SHA512在線加密|GEEKAPP開發(fā)者在線工具
5.4 HMAC
HMAC全稱:散列消息鑒別碼。HMAC加密算法是一種安全的基于加密hash函數(shù)和共享密鑰的消息認證協(xié)議。實現(xiàn)原理是用公開函數(shù)和密鑰產(chǎn)生一個固定長度的值作為認證標識,用這個標識鑒別消息的完整性。
python實現(xiàn)代碼:
new(key,msg=None,digestmod)方法
-
創(chuàng)建哈希對象
-
key和digestmod參數(shù)必須指定,key和msg(需要加密的內(nèi)容)均為bytes類型,digestmod指定加密算法,比如‘md5’,'sha1'等
對象digest()方法:返回bytes類型哈希值
對象hexdigest()方法:返回十六進制哈希值
import hmac
import hashlib
key = "key".encode()
text = "msb".encode()
m = hmac.new(key, text, hashlib.sha256)
print(m.digest())
print(m.hexdigest())
5.5 DES
DES全稱:數(shù)據(jù)加密標準(Data Encryption Standard),屬于對稱加密算法。DES是一個分組加密算法,典型的DES以64位為分組對數(shù)據(jù)加密,加密和解密用的是同一個算法。它的密鑰長度是56位(因為每個第8位都用作奇偶校驗),密鑰可以是任意的56位數(shù),而且可以任意時候改變。
Js逆向時,DES加密的搜索關鍵詞有DES、mode、padding等。
python實現(xiàn)代碼:
# pyDes需要安裝
from pyDes import des, CBC, PAD_PKCS5
import binascii
# 秘鑰
KEY = 'dsj2020q'
def des_encrypt(s):
"""
DES 加密
:param s: 原始字符串
:return: 加密后字符串,16進制
"""
secret_key = KEY
iv = secret_key
k = des(secret_key, CBC, iv, pad=None, padmode=PAD_PKCS5)
en = k.encrypt(s, padmode=PAD_PKCS5)
return binascii.b2a_hex(en).decode()
?
?
def des_decrypt(s):
"""
DES 解密
:param s: 加密后的字符串,16進制
:return: 解密后的字符串
"""
secret_key = KEY
iv = secret_key
k = des(secret_key, CBC, iv, pad=None, padmode=PAD_PKCS5)
de = k.decrypt(binascii.a2b_hex(s), padmode=PAD_PKCS5)
return de.decode()
?
?
text = 'msb'
secret_str = des_encrypt(text)
print(secret_str)
?
clear_str = des_decrypt(secret_str)
print(clear_str)
5.5 AES
AES全程:高級加密標準,在密碼學中又稱Rijndael加密法,是美國聯(lián)邦政府采用的一種區(qū)塊加密標準。
AES也是對稱加密算法,如果能夠獲取到密鑰,那么就能對密文解密。
Js逆向時,AES加密的搜索關鍵詞有AES、mode、padding等。
python代碼實現(xiàn)之前
pip install pycryptodome
python實現(xiàn)代碼:
import base64
from Crypto.Cipher import AES
?
# AES
# 需要補位,str不是16的倍數(shù)那就補足為16的倍數(shù)
def add_to_16(value):
while len(value) % 16 != 0:
value += '\0'
return str.encode(value) # 返回bytes
?
# 加密方法
def encrypt(key, text):
aes = AES.new(add_to_16(key), AES.MODE_ECB) # 初始化加密器
encrypt_aes = aes.encrypt(add_to_16(text)) # 先進行aes加密
encrypted_text = str(base64.encodebytes(encrypt_aes), encoding='utf-8')
return encrypted_text
?
# 解密方法
def decrypt(key, text):
aes = AES.new(add_to_16(key), AES.MODE_ECB) # 初始化加密器
base64_decrypted = base64.decodebytes(text.encode(encoding='utf-8'))
decrypted_text = str(aes.decrypt(base64_decrypted), encoding='utf-8').replace('\0', '') # 執(zhí)行解密密并轉(zhuǎn)碼返回str
return decrypted_text
5.6 RSA
RSA全稱:Rivest-Shamir-Adleman, RSA加密算法是一種非對稱加密算法,在公開密鑰加密和電子商業(yè)中RSA被廣泛使用,它被普遍認為是目前最優(yōu)秀的公鑰方案之一。RSA是第一個能同時用于加密和數(shù)字簽名的算法,它能夠抵抗目前為止已知的所有密碼攻擊。
注意Js代碼中的RSA常見標志setPublickey。
算法原理參考:https://www.yht7.com/news/184380
實現(xiàn)代碼之前先安裝 :文章來源:http://www.zghlxwxcb.cn/news/detail-831883.html
pip install pycryptodome
python代碼實現(xiàn):文章來源地址http://www.zghlxwxcb.cn/news/detail-831883.html
import base64
from Crypto.Cipher import PKCS1_v1_5
from Crypto import Random
from Crypto.PublicKey import RSA
?
?
# ------------------------生成密鑰對------------------------
def create_rsa_pair(is_save=False):
"""
創(chuàng)建rsa公鑰私鑰對
:param is_save: default:False
:return: public_key, private_key
"""
f = RSA.generate(2048)
private_key = f.exportKey("PEM") # 生成私鑰
public_key = f.publickey().exportKey() # 生成公鑰
if is_save:
with open("crypto_private_key.pem", "wb") as f:
f.write(private_key)
with open("crypto_public_key.pem", "wb") as f:
f.write(public_key)
return public_key, private_key
?
?
def read_public_key(file_path="crypto_public_key.pem") -> bytes:
with open(file_path, "rb") as x:
b = x.read()
return b
?
?
def read_private_key(file_path="crypto_private_key.pem") -> bytes:
with open(file_path, "rb") as x:
b = x.read()
return b
?
?
# ------------------------加密------------------------
def encryption(text: str, public_key: bytes):
# 字符串指定編碼(轉(zhuǎn)為bytes)
text = text.encode("utf-8")
# 構建公鑰對象
cipher_public = PKCS1_v1_5.new(RSA.importKey(public_key))
# 加密(bytes)
text_encrypted = cipher_public.encrypt(text)
# base64編碼,并轉(zhuǎn)為字符串
text_encrypted_base64 = base64.b64encode(text_encrypted).decode()
return text_encrypted_base64
?
?
# ------------------------解密------------------------
def decryption(text_encrypted_base64: str, private_key: bytes):
# 字符串指定編碼(轉(zhuǎn)為bytes)
text_encrypted_base64 = text_encrypted_base64.encode("utf-8")
# base64解碼
text_encrypted = base64.b64decode(text_encrypted_base64)
# 構建私鑰對象
cipher_private = PKCS1_v1_5.new(RSA.importKey(private_key))
# 解密(bytes)
text_decrypted = cipher_private.decrypt(text_encrypted, Random.new().read)
# 解碼為字符串
text_decrypted = text_decrypted.decode()
return text_decrypted
?
?
if __name__ == "__main__":
# 生成密鑰對
# create_rsa_pair(is_save=True)
# public_key = read_public_key()
# private_key = read_private_key()
public_key, private_key = create_rsa_pair(is_save=False)
?
# 加密
text = "msb"
text_encrypted_base64 = encryption(text, public_key)
print("密文:", text_encrypted_base64)
?
# 解密
text_decrypted = decryption(text_encrypted_base64, private_key)
print("明文:", text_decrypted)
到了這里,關于【W(wǎng)eb】從零開始的js逆向?qū)W習筆記(上)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!