一,導言
// global是node中的關鍵字(全局變量),在node中調用其中的元素時,可以直接引用,不用加global前綴,和瀏覽器中的window類似;在瀏覽器中可能會使用window前綴:window.location.href,也可能省略window直接調用location.href,這兩種情況通過將window賦予global一樣的全局屬性便都可以正常調用了window = global; // 將window變成node中的全局變量
- 電腦上安裝node.js后,就可以執行JavaScript(編譯器,相當于python裝CPython解釋器)
- 安裝node.js時,會自動安裝npm(第三方包管理器,相當于python中的pip)
什么是純凈V8:
在純凈V8中,除了V8引擎本身之外,沒有其他瀏覽器相關的組件和功能
。因此,開發人員可以使用純凈V8來構建獨立的JavaScript應用程序(例如Node.js),而無需依賴于任何瀏覽器的特定功能和API。
什么是BOM和DOM:BOM包含DOM
JavaScript內置對象,所有不在內置對象的都不是v8引擎自帶的,比如Regexp/Object/Proxy是v8有的, global不屬于v8屬于node特有的
,查詢連接為:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects
二,什么是補瀏覽器環境?
**瀏覽器環境:**是指JS代碼在瀏覽器中的運行時環境,包括V8自動構建的對象(即ECMAScript的內容,如Date、Array),瀏覽器內置傳遞給V8的操作DOM和BOM的對象(如document、navigator)。
**Node環境:**是基于V8引擎的Js運行時環境,它包括V8與其自己的內置API,如fs, http, path。
因此,補瀏覽器環境其實是補瀏覽器有而node沒有的環境,即補BOM和DOM對象。此外,Node環境和瀏覽器還具有一些區別,比如window對象區別、this指向區別、Js引擎區別,以及一些DOM操作區別。
三,為什么要補環境?
一般情況下,將加密算法扣下來能夠直接執行,但是如果檢測了瀏覽器指紋(由于Node環境與瀏覽器環境之間存在差異,會導致部分JS代碼在瀏覽器中運行的結果 與在node中運行得到的結果不一樣),那就比較難了。
所以需要 “補瀏覽器環境”,使得扣出來的 “js加密算法代碼” 在Node環境中運行得到的加密值,與其在 瀏覽器環境中運行得到的加密值一致。
環境對JS代碼的影響跳不過這兩種方式:
- 用來判斷,改變邏輯
- 值參與加密運算
四,常被檢測的環境
在常被檢測的環境中,有window、location、document、navigator、canvas、native方法檢測等,除了這些屬性外,還有針對自動化痕跡的檢查(比如對chromedriver屬性檢測)、Node環境的檢測,以及瀏覽器指紋檢測、TLS指紋校驗、驅動檢測、異常堆棧調用檢測等。
window檢測:
- window是否為方法
- window對象是否freeze
- 各屬性檢測
location檢測:
- hostname
- protocol
- host
- hash
- origin
navigator檢測:
- AppName
- AppVersion
- cookieEnabled
- language
- userAgent
- product
- platform
- plugins瀏覽器插件
- javaEnabled() 方法
- taintEnabled() 方法
document檢測:
- referrer
- cookie
- createElement() 方法
canvas指紋:
- 不同類型圖片的canvas指紋應當不一樣,如 .jpg、.png
- 不同質量quality的canvas指紋應該不一樣
- 不同屬性的canvas指紋應該不一樣
- 同一個條件的canvas多次繪制時應該保持一致
瀏覽器指紋信息:
- window.screen屏幕分辨率/寬高
- navigator.useragent
- location.href/host
- navigator.platform平臺、語言等信息
- canvas 2D圖像指紋
- navigator.plugin 瀏覽器插件信息
- webgl 3D圖像指紋
- 瀏覽器字體信息
- 本地存儲的cookie信息
五,確定需要補那些環境?
要想 “補瀏覽器環境”,首先我們得知道 “js加密算法代碼” 到底使用了哪些瀏覽器環境API,然后再對應去補上這些環境;
5.1 通過undefined報錯去分析
通過運行程序后的undefined報錯一點一點的去補一些環境,這種方式是非常掉頭發的。
5.2 使用Proxy
Proxy是ES6提供的代理器(在瀏覽器和Node中均可使用),用于創建一個對象的代理,從而實現基本操作的攔截和自定義。 它可以代理任何類型的對象,包括原生數組,函數,甚至另一個代理;擁有遞歸套娃的能力!也就是說 我們代理某個對象后,我們就成了它的中間商,任何JS代碼對它的任何操作都可以被我們所攔截!
使用 Proxy 對全局遍歷window、document、navigator等常見環境檢測點進行代理,攔截代理對象的讀取、函數調用等操作,并通過控制臺輸出,這樣就能夠實現檢測環境自吐的功能,后續再針對吐出來的環境統一進行補環境,這樣就會方便的多。
Proxy基礎用法:
先定義一個 target 對象,即目標對象;同時定義 handler 對象,handler 聲明了代理 target 的指定行為。
// 目標對象(被代理對象)
var target = {name: 'JACK',age: 18,
};// 代理行為對象(對目標對象進行取值或賦值行為時,會進入此對象的方法)
var handler = {get: function(target, property, receiver) {console.log("get: ", target, property, target[property]);return target[property]; },set: function(target, property, value) {console.log("set: ", target, property, value);return Reflect.set(...arguments);}
};// 使用 Proxy 構造函數實例出新的target對象
var target = new Proxy(target, handler)// 取值操作 會進入handler中的get方法 并打印
target.name // get: {name: 'JACK', age: 18} name JACK
// 賦值操作 會進入handler中的set方法 并打印
target.age = 25 // set: {name: 'JACK', age: 18} age 25
下圖為Proxy的使用方法,以及補環境代碼框架,主體分為三大部分:
當底部的代碼有用到某個在瀏覽器中的對象時,就會在控制臺輸出對應的內容(前提是該對象被掛上了代理,并且觸發的是get或set方法)
運行結果如下:
缺少的環境一目了然。后面就是缺啥補啥,只要將上圖中顯示undefined的對象及其屬性補充在第二部分代碼后面即可。(location和navigator等要補充的值在Console中查看即可)
示例代碼如下:
/* 1,框架代理功能放在頂部 */
var handlerProxy = function (object) {return new Proxy(object, {get: function(target, property, receiver){console.log("get", target, property, target[property]);return target[property];},set: function(target, property, value){console.log("set", target, property, value);return Reflect.set(...arguments);}})
};/* 2,給常見的環境對象掛上代理(掛上代理后在不影響原功能的情況下,當對象被調用時可以在控制臺輸出結果) */
window = global;
navigator = class navigator{};
document = class document{};
location = class location{};
window = handlerProxy(window);
navigator = handlerProxy(navigator);
document = handlerProxy(document);
location = handlerProxy(location);/* 3,原生待補環境的js放在底部,調試輸出觀察 */
location.href = "https://blog.csdn.net/weixin_43411585";
location.protocol = "https:";
六,補環境的方法
6.1 手補
以下舉兩個例子:
1:當JS代碼會判斷你當前環境的window對象是否有toString方法
if (window.toString)
{//do some thing; 修改全局變量啥的
}
這時,只需要將 window.toString 賦值為true即可:
window = {};
window.toString = true;
這樣寫沒有問題,只是個邏輯,如果還判斷了是否為函數:
if (typeof window.toString === 'function')
{//do some thing; 修改全局變量啥的
}
這時我們依葫蘆畫瓢將其定義為函數:
window.toString = function(){};
如果對返回值進行了判斷或者參與了加密運算,則直接在瀏覽器的控制臺上運行看看返回值是啥:
再依照它的結果補上去就可以了:
window.toString = function() {return "[object Window]"};
2:下面是某cdn的部分代碼:
document["createElement"]("img")["src"] = "/R=1&e=" + Math["random"]();
這種情況下,當前node環境如果沒有 createElement 函數,肯定是會報錯的。那該怎么去補呢,我們來分析:
document[“createElement”] 是一個函數,你可以這樣定義:
document = {};
document.createElement = function(){};
它傳遞了一個實參 “img”,因此還需要參數,像這樣:
document.createElement = function(img){};
這樣肯定還不行,document"createElement" 代碼后面加了個 [“src”],那說明
-
當實參為 "img"時,有返回值。
-
返回值是一個object類型。
document.createElement = function(img){if (img === "img"){return {};}
};
上面的代碼就可以滿足條件了,如果你想更完美,可以像下面這樣:
document = {};
document.createElement = function(img)
{if (img === "img"){return {src:""};}
}
在確定需要補什么內容時,直接在在瀏覽器上輸出相應的值即可。
6.2 借助jsdom
npm install node-gyp@latest sudo npm explore -g npm -- npm i node-gyp@latest // 更新npm
npm install jsdom -g
注意:上述安裝成功后已可以模擬瀏覽器環境,由于今日頭條中依賴canvas包,這里一并下載
npm install canvas -g
jsdom入門示例:
// npm install jsdomconst jsdom = require("jsdom"); // 引入 jsdom
const { JSDOM } = jsdom; // 引出 JSDOM 類, 等同于 JSDOM = jsdom.JSDOM
const dom = new JSDOM(`<!DOCTYPE html><p>Hello world</p>`); // 創建DOM對象
console.log(dom.window.document.querySelector("p").textContent); // Hello world
配置DOM:
const dom = new JSDOM(`<!DOCTYPE html><p>Hello world</p>`, {url: "https://example.org/", // window.location,document.URLreferrer: "https://example.com/", // document.referrercontentType: "text/html", // document.contentTypeuserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36 Edg/105.0.1343.42", // UAincludeNodeLocations: true // 保留由HTML解析器生成的位置信息,允許使用nodeLocation()方法
});
執行JS:
const jsdom = require("jsdom");
const { JSDOM } = jsdom;
const dom = new JSDOM(`<body><script>document.body.appendChild(document.createElement("hr"));console.log("hello world");</script></body>`, { runScripts: "dangerously" } // 需要配置runScripts 否則不運行 JS);
// hello world
設置cookie:
const cookieJar = new jsdom.CookieJar(store, options);
const dom = new JSDOM(``, { cookieJar });
補充環境變量實例:
const jsdom = require("jsdom");
const {JSDOM} = jsdom;const resourceLoader = new jsdom.ResourceLoader({userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.55 Safari/537.36"
});const html = `<!DOCTYPE html><p>Hello world</p>`;
const dom = new JSDOM(html, {url: "https://www.toutiao.com",referrer: "https://example.com/",contentType: "text/html",resources: resourceLoader,
});console.log(dom.window.location)
console.log(dom.window.navigator.userAgent)
console.log(dom.window.document.referrer)window = global; const params = {location: {hash: "",host: "www.toutiao.com",hostname: "www.toutiao.com",href: "https://www.toutiao.com",origin: "https://www.toutiao.com",pathname: "/",port: "",protocol: "https:",search: "",},test:{hahaha:"nothing at all!!!"}navigator: {appCodeName: "Mozilla",appName: "Netscape",appVersion: "5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.82 Safari/537.36",cookieEnabled: true,deviceMemory: 8,doNotTrack: null,hardwareConcurrency: 4,language: "zh-CN",languages: ["zh-CN", "zh"],maxTouchPoints: 0,onLine: true,platform: "MacIntel",product: "Gecko",productSub: "20030107",userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.82 Safari/537.36",vendor: "Google Inc.",vendorSub: "",webdriver: false}
};Object.assign(window,params); // 為window補充環境變量// 使用window調用或直接調用
console.log(window.location.href)
console.log(location.href)