博客文章:深入理解JS逆向代理與環境監測
1. 引言
首先要明確JavaScript(JS)在真實網頁瀏覽器環境和Node.js環境中有很多使用特性的區別。尤其是在環境監測和對象原型鏈的檢測方面。本文將探討如何使用JS的代理(Proxy)模式來手動補充環境,Node環境和瀏覽器this
環境,以及如何通過原型鏈檢測來增強代碼的安全性以及在。
2. 代理補環境
代理是ES6引入的一種新特性,它允許你定義對象的行為,例如屬性的讀取和設置。以下是一個簡單的代理補環境的實現:
// 代理補環境(缺啥補啥)
function vmProxy(object, objName) {// 創建代理對象,捕獲對對象的訪問和賦值操作return new Proxy(object, {get: function(target, property, receiver) {// 打印屬性訪問信息console.log(objName, "get: ", property, target[property]);// 返回目標對象的屬性值return target[property];},set: function(target, property, value) {// 打印屬性賦值信息console.log(objName, "set: ", property, value);// 使用Reflect.set實現屬性賦值return Reflect.set(...arguments);}});
}// 模擬瀏覽器環境
navigator = {userAgent: 'qh'
};
document = {};// 使用代理來增強navigator和document對象
navigator = vmProxy(navigator, 'navigator');
document = vmProxy(document, 'document');// 測試代理效果
console.log(navigator.userAgent); // 應輸出代理訪問信息
console.log(navigator.platform); // 將觸發代理的get方法
console.log(document.cookie); // 將觸發代理的get方法
console.log(document.createElement); // 將觸發代理的get方法
3. Node環境與瀏覽器this
環境監測
在Node.js中,global
對象是全局對象,而在瀏覽器中,window
是全局對象。檢測這些環境可以通過以下方式實現:
// 確保window指向全局對象
window = globalThis;
// 補充window對象的方法
window['addEventListener'] = function() {};
// 初始化window的navigator和document屬性
window['navigator'] = {};
window['document'] = {};// 使用代理來增強window對象
// 錯誤寫法1:
// window = vmProxy(window, 'window'); // 代理器會影響對象本身的指向,代理window對象本身并不是錯誤,但關鍵在于不應該覆蓋原有的window對象。代理可以用來增強或監測window對象的行為,但不應該改變其身份。
// 錯誤寫法2:
// window ={'addEventListener': function() {},'navigator':{},'document' {}}; // 直接賦值window為一個新對象會覆蓋原有的window對象,這會丟失所有原有的全局屬性和方法,包括繼承來的屬性。
// 錯誤寫法3 重新賦值會影響指向 將window賦值為一個空對象同樣會覆蓋原有的window對象,導致丟失所有屬性和方法。
// window={}// 測試eval.call方法是否正確指向window
!function (){function test(){// 測試eval.call的this指向console.log(eval["call"](undefined, this) === window); // 應輸出true}test.apply(null);
}();
這段代碼的功能是測試eval.call
方法的this
指向是否正確。在JavaScript中,eval
函數可以計算一個字符串表達式的值。當使用call
方法調用eval
時,可以指定this
的值。在這個例子中,想要測試eval.call
是否能夠正確地將this
指向window
對象得到true的結果證明現在node環境和瀏覽器環境是一致的。
下面是對上面代碼的詳細拆分和解讀,如果已經理解就跳過。
錯誤寫法1解釋:
// 錯誤寫法1:
// window = vmProxy(window, 'window'); // 代理器會影響對象本身的指向
這行代碼是錯誤的,因為window
對象是全局對象,其原型鏈上有很多內置屬性和方法。將window
重新賦值為vmProxy
的返回值會切斷window
與原有原型鏈的聯系,導致丟失原有的全局屬性和方法。此外,這行代碼試圖將window
對象自身作為代理的目標,這在邏輯上是有問題的,因為window
對象本身不應該被代理。如下圖高亮紫色是原有全局屬性是繼承來的,
在瀏覽器中打印window對象時,你可能會注意到屬性顏色的差異,這通常是由于瀏覽器的開發者工具中的顏色編碼。在Chrome的開發者工具中,全局對象window的屬性通常分為兩類:
原有全局屬性:這些屬性是window對象直接定義的,通常是淺色顯示,表示它們是window對象的自有屬性,而不是通過原型鏈繼承的。
繼承來的屬性:這些屬性是window對象通過原型鏈從其原型Window.prototype繼承的,通常會以高亮紫色顯示,以區分自有屬性。
例如,navigator對象是window對象的一個自有屬性,而navigator對象本身繼承自Navigator的屬性。在瀏覽器的控制臺中打印window對象時,你會看到類似下面的結構:
Window {window: Window { ... },self: Window { ... },document: document,name: "name",location: Location { ... },history: History { ... },navigator: Navigator { ... }, // 繼承自 Navigator 的屬性將顯示為高亮紫色// ... 其他自有屬性和繼承屬性
}
在這個例子中,document、location、history等是window對象的自有屬性,而navigator對象的屬性,如userAgent,是繼承自Navigator的屬性。
代碼示例:
以下是如何在控制臺中查看這些屬性的示例代碼:
console.dir(window); // 打印window對象及其屬性
使用console.dir可以打印出對象的詳細信息,包括原型鏈上的屬性。
錯誤寫法2解釋:
// 錯誤寫法2:
// window ={'addEventListener': function() {},'navigator':{},'document' {}}; // 直接賦值window為一個新對象會覆蓋原有的window對象,這會丟失所有原有的全局屬性和方法,包括繼承來的屬性。
這行代碼同樣是錯誤的,因為它試圖將window
重新定義為一個具有單個屬性addEventListener
的對象,這個屬性的值是一個空字符串。正確的做法是為window
添加方法,而不是重新定義window
對象。
錯誤寫法3解釋:
// 錯誤寫法3 重新賦值會影響指向 將window賦值為一個空對象同樣會覆蓋原有的window對象,導致丟失所有屬性和方法。
// window = {};
這行代碼是錯誤的,因為它將window
重新賦值為一個空對象,這會覆蓋原有的全局window
對象,導致所有原有的全局屬性和方法丟失。
衡量錯誤的標準:
在JS逆向中,衡量錯誤的標準是在Node環境中是否成功模擬了瀏覽器環境。正確的做法應該是在不破壞原有window
對象的基礎上,補充或修改其屬性和方法,以模擬瀏覽器環境。
加代理正確的做法1:
// 確保window指向全局對象
window = globalThis;// 補充window對象的方法
if (typeof window.addEventListener === 'undefined') {window['addEventListener'] = function() {};
}// 初始化window的navigator和document屬性,但不要覆蓋原有的屬性
if (!window.navigator) {window['navigator'] = {};
}
if (!window.document) {window['document'] = {};
}// 使用代理來增強window對象,但不要重新賦值window
window = vmProxy(window, 'window');
在這個修正的代碼中,我們首先檢查window
對象上是否已經有addEventListener
方法,如果沒有,我們才添加它。同樣,我們檢查navigator
和document
是否存在,如果不存在,我們才初始化它們。最后,我們使用vmProxy
來增強window
對象,而不是重新賦值它。
加代理正確的做法2:
// 創建window的代理,而不是重新賦值window
const originalWindow = window;
const proxiedWindow = new Proxy(originalWindow, {get(target, property, receiver) {if (property === 'navigator') {console.log('Accessing navigator');}return Reflect.get(target, property, receiver);},set(target, property, value, receiver) {console.log(`Setting ${property} to ${value}`);return Reflect.set(target, property, value, receiver);}
});// 使用代理對象進行操作,而不是直接操作window
proxiedWindow.navigator.userAgent; // 這將觸發get陷阱,并打印日志
創建了一個window的代理,而不是直接修改window對象。這樣,我們可以在不改變window對象本身的情況下,監測和增強其行為。
測試eval.call方法:
!function (){function test(){// 測試eval.call的this指向console.log(eval["call"](undefined, this) === window); // 應輸出true}test.apply(null);
}();
這個測試函數test
使用apply
方法將this
指向null
,然后通過eval.call
將this
指向window
。如果eval.call
正確地將this
指向了window
,那么console.log
應該輸出true
。
4. 原型鏈檢測
原型鏈是JS中實現繼承的關鍵機制。## 4. 原型鏈檢測的深入分析
原型鏈是JavaScript中實現繼承的核心機制。每個JavaScript對象都有一個原型對象,這個原型對象可以是另一個對象或者null
。當訪問一個對象的屬性時,如果該屬性在對象上不存在,JavaScript引擎會沿著原型鏈向上查找,直到找到該屬性或到達原型鏈的末端(null
)。
代碼拆分
定義構造函數和原型屬性
// 定義構造函數Navigator
Navigator = function Navigator(){};
// 在Navigator.prototype上定義userAgent屬性
Object.defineProperty(Navigator.prototype,'userAgent',{set: undefined, // 不允許賦值enumerable: true, // 可枚舉configurable: true, // 可配置get: function() {return "Custom UserAgent"; // 自定義getter函數} // 自定義getter}
);
// 創建navigator對象,其原型指向Navigator.prototype
navigator = {};
navigator.__proto__ = Navigator.prototype;// 檢測navigator的屬性描述符
console.log(Object.getOwnPropertyDescriptors(navigator)); // 應輸出undefined
console.log(Object.getOwnPropertyDescriptors(Navigator.prototype)); // 顯示實際的屬性描述符
截圖示例:在瀏覽器的控制臺中,可以看到Navigator
構造函數和其prototype
上的userAgent
屬性定義。
創建并鏈接自定義navigator
對象
// 創建navigator對象,其原型指向Navigator.prototype
var navigator = {};
Object.setPrototypeOf(navigator, new Navigator()); // 更現代的方法來設置原型
截圖示例:在控制臺中,通過Object.getPrototypeOf(navigator)
可以看到navigator
的原型現在指向Navigator.prototype
。
屬性描述符檢測
// 檢測navigator的屬性描述符
console.log(Object.getOwnPropertyDescriptors(navigator)); // 應輸出undefined,因為navigator上沒有直接定義userAgent屬性
console.log(Object.getOwnPropertyDescriptors(Navigator.prototype)); // 顯示實際的屬性描述符,包括userAgent的定義
截圖示例:在控制臺中,Object.getOwnPropertyDescriptors
的調用結果展示了Navigator.prototype
上的userAgent
屬性描述符。
JS逆向原型鏈相關知識
在JavaScript逆向工程中,了解原型鏈對于分析和修改代碼行為至關重要。以下是一些與原型鏈檢測相關的逆向知識點:
- 原型鏈遍歷:逆向工程師可以通過遍歷對象的原型鏈來查找對象的來源和構造方式。
- 原型鏈污染:通過修改對象的原型鏈,可以引入新的行為或屬性,這在某些情況下可以用于繞過安全限制。
- 構造函數欺騙:通過修改構造函數的
prototype
,可以改變通過該構造函數創建的所有新對象的行為。 - 屬性攔截:使用
Proxy
對象,可以在訪問或設置屬性時進行攔截和自定義行為,這可以用來模擬或監測對象的行為。 - 環境檢測:通過檢測對象的原型鏈,可以判斷代碼運行在何種環境中(瀏覽器或Node.js),并據此調整代碼行為。
通過這些技術,逆向工程師可以深入了解和操縱JavaScript代碼的運行時行為,實現代碼審計、安全測試或功能增強。然而,這些技術也應謹慎使用,以避免潛在的安全風險和代碼維護問題。
5. 環境監測點案例
以下是一些Node和瀏覽器環境監測點的案例:
- 監測全局對象類型:
typeof global !== 'undefined' ? 'node' : 'browser'
- 監測Node.js特有的模塊:
require.main === module
- 監測瀏覽器特有的對象:
typeof window !== 'undefined' && !!window.document
- 監測瀏覽器的BOM和DOM:
typeof document !== 'undefined' && !!document.createElement
- 監測環境支持的ES6特性:
'startsWith' in String.prototype
7. 高級監測點:使用eval
和Proxy
進行環境監測
在JavaScript中,eval
函數允許你執行字符串中的代碼。然而,使用eval
通常被認為是不安全的,因為它可以執行任意代碼。但是,在某些情況下,我們可以通過修改eval
的行為來增強環境監測。以下是一個示例代碼,展示了如何重寫eval.call
方法,以監測和區分代碼執行環境:
// 重寫eval.call方法以監測執行環境
eval['call'] = function (){debugger; // 啟動調試模式,便于開發者調試if (arguments[1].toString() === '[object Window]'){debugger; // 再次啟動調試模式,如果this指向Window對象// 如果調用環境是瀏覽器的window對象,則直接返回windowreturn window;} else {// 否則,執行原始的eval函數return eval(arguments);}
};
代碼解釋
eval['call']
: 我們通過eval['call']
訪問eval
函數的call
方法,并對其進行重寫。debugger
: 這是一個調試語句,當代碼執行到這里時,如果正在調試模式下,執行會暫停,允許開發者檢查當前的執行環境和變量狀態。arguments[1].toString()
:arguments
對象包含了調用函數時傳入的所有參數。在這里,我們檢查arguments[1]
(即this
的值),并使用toString()
方法獲取它的類型描述。'[object Window]'
: 這是一個特定的字符串,用來檢測this
是否指向瀏覽器的window
對象。return window
: 如果檢測到this
是window
對象,我們直接返回window
,這樣eval.call
調用的結果將總是指向全局的window
對象,而不是局部作用域中的this
。
使用場景
這種技術可以用于確保在執行動態代碼時,this
總是指向預期的全局對象,從而避免潛在的作用域問題。此外,它還可以用于調試和測試,幫助開發者更好地理解代碼在不同環境下的行為。
7.與局部加密方法對象導出到全局對象的區別
使用代理補環境和原型鏈檢測是一種在運行時動態地修改對象的行為和結構的方法。與之相比,將局部加密方法對象導出到全局對象是一種靜態的修改,通常在代碼的編寫階段就已經確定。代理補環境提供了一種靈活的方式來監測和修改對象的行為,而局部加密方法對象的導出則是一種更靜態、更難以在運行時改變的方法。
結語
通過本文的探討,我們了解到了JS代理的強大功能以及如何使用它來監測和增強JS運行環境。同時,我們也學習了如何通過原型鏈檢測來提高代碼的安全性。這些技術不僅能夠幫助開發者更好地理解和控制JS代碼的行為,還能夠在開發過程中提供更多的靈活性和安全性。