一,导言
// 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)