Skip to content
On this page

了解什么是补环境

在做网络爬虫这一行业的小伙伴们肯定几乎都听说过 补环境 这个名词,补环境完整名称应该为 补浏览器环境,在当前就业环境下,补浏览器环境 是爬虫工作者升职加薪的必备技能,也是工作中经常遇到的操作。

什么是补浏览器环境?

  • 浏览器环境:这里拿 Chrome 浏览器举例,是指 JS 代码在浏览器中的运行时环境,它包括 V8 引擎自动构建的对象(即 ECMAScript 的内容,例如 Date 对象、Array 对象),还有浏览器(内置)传递给 V8 引擎的 DOM 对象(例如 document 对象)和 BOM 对象(例如 navigator 对象)。
  • Node 环境:是基于 V8 引擎的 JS 运行时环境,它包括 V8 引擎与其自己的内置 API,例如 fs、path、http等模块。

V8 引擎是 Google 自己开发的高性能 JavaScript 和 WebAssembly 引擎,用 C 编写,它实现 ECMAScript 和 WebAssembly,可独立运行或嵌入到任何 C 程序中,如 Chrome 和 Node.js等。

Safari 浏览器使用的是 JavaScriptCore,Firefox 浏览器使用的是 SpiderMonkey。

浏览器环境Node 环境的异同点可以简单的概括如下图:

可以看到 Node 环境缺少了浏览器环境中的 BOM 和 DOM,而且多了 Node 内置 API,所以我们所说的 补浏览器环境 就是做了 2 件事:(1)补充 BOM 和 DOM;(2)删除 Node 环境独有的属性,因为在真实浏览器环境中是不会存在 Node 内置 API的。

为什么要补浏览器环境?

这里举一个例子,有一天小宝接到一个任务,需要获取某个网站上的所有一些数据,经小宝一顿分析,发现接口有一些参数加密了,结果小宝三下五除二的就把加密算法给扣了出来,代码如下:

js
const _0x53175f=_0x1726;(function(_0x418c5f,_0x4fb857){const _0x162ec2=_0x1726,_0x8444fc=_0x418c5f();while(!![]){try{const _0x27b1eb=parseInt(_0x162ec2(0x153))/0x1*(-parseInt(_0x162ec2(0x13c))/0x2)+parseInt(_0x162ec2(0x13d))/0x3+parseInt(_0x162ec2(0x154))/0x4+parseInt(_0x162ec2(0x146))/0x5+-parseInt(_0x162ec2(0x14a))/0x6+parseInt(_0x162ec2(0x13f))/0x7*(-parseInt(_0x162ec2(0x14e))/0x8)+parseInt(_0x162ec2(0x140))/0x9*(parseInt(_0x162ec2(0x14d))/0xa);if(_0x27b1eb===_0x4fb857)break;else _0x8444fc['push'](_0x8444fc['shift']());}catch(_0x1975de){_0x8444fc['push'](_0x8444fc['shift']());}}}(_0x5b6a,0x20833),navigator={});function getParam(){const _0xad3f6a=(function(){let _0x1fcd09=!![];return function(_0x50f72e,_0x195b87){const _0x5c9c27=_0x1fcd09?function(){const _0x413ce5=_0x1726;if(_0x195b87){const _0x60cd63=_0x195b87[_0x413ce5(0x14f)](_0x50f72e,arguments);return _0x195b87=null,_0x60cd63;}}:function(){};return _0x1fcd09=![],_0x5c9c27;};}()),_0x1489c7=_0xad3f6a(this,function(){const _0xe72e2b=_0x1726;return _0x1489c7[_0xe72e2b(0x147)]()[_0xe72e2b(0x14b)]('(((.+)+)+)+$')[_0xe72e2b(0x147)]()[_0xe72e2b(0x143)](_0x1489c7)[_0xe72e2b(0x14b)]('(((.+)+)+)+$');});_0x1489c7();const _0xedbf3f=(function(){let _0x1a2765=!![];return function(_0x4b326c,_0x9c6914){const _0x54a9fe=_0x1a2765?function(){const _0x360ad2=_0x1726;if(_0x9c6914){const _0xc67d04=_0x9c6914[_0x360ad2(0x14f)](_0x4b326c,arguments);return _0x9c6914=null,_0xc67d04;}}:function(){};return _0x1a2765=![],_0x54a9fe;};}()),_0x5bbf02=_0xedbf3f(this,function(){const _0x2c947a=_0x1726,_0x54f968=function(){const _0x42f25f=_0x1726;let _0x4fd223;try{_0x4fd223=Function(_0x42f25f(0x150)+_0x42f25f(0x14c)+');')();}catch(_0x5da32f){_0x4fd223=window;}return _0x4fd223;},_0x4841a9=_0x54f968(),_0x5c2908=_0x4841a9[_0x2c947a(0x149)]=_0x4841a9[_0x2c947a(0x149)]||{},_0x3f77e7=[_0x2c947a(0x148),_0x2c947a(0x141),_0x2c947a(0x142),'error','exception',_0x2c947a(0x152),_0x2c947a(0x145)];for(let _0x174dca=0x0;_0x174dca<_0x3f77e7[_0x2c947a(0x144)];_0x174dca++){const _0x3fb8d2=_0xedbf3f[_0x2c947a(0x143)][_0x2c947a(0x151)][_0x2c947a(0x13e)](_0xedbf3f),_0xfb5875=_0x3f77e7[_0x174dca],_0x626e11=_0x5c2908[_0xfb5875]||_0x3fb8d2;_0x3fb8d2['__proto__']=_0xedbf3f[_0x2c947a(0x13e)](_0xedbf3f),_0x3fb8d2[_0x2c947a(0x147)]=_0x626e11[_0x2c947a(0x147)][_0x2c947a(0x13e)](_0x626e11),_0x5c2908[_0xfb5875]=_0x3fb8d2;}});_0x5bbf02();let _0x4d485c=navigator?navigator:'';return btoa(_0x4d485c['userAgent']);}function _0x5b6a(){const _0x42b6ae=['length','trace','821465HjfUxa','toString','log','console','1388694XyFVml','search','{}.constructor(\x22return\x20this\x22)(\x20)','2248980qsrzJI','803696szEsKC','apply','return\x20(function()\x20','prototype','table','6HGaZls','380152zmwejH','86782jHSrnc','723597KZUJHT','bind','7bwxZzn','9soShnj','warn','info','constructor'];_0x5b6a=function(){return _0x42b6ae;};return _0x5b6a();}function _0x1726(_0x358b76,_0x28fc06){const _0x135160=_0x5b6a();return _0x1726=function(_0x430df5,_0x553c47){_0x430df5=_0x430df5-0x13c;let _0x34377a=_0x135160[_0x430df5];return _0x34377a;},_0x1726(_0x358b76,_0x28fc06);}console[_0x53175f(0x148)](getParam());

接下来展示下这段代码在不同环境写直接运行的结果:

  • 浏览器环境:TW96aWxsYS81LjAgKE1hY2ludG9zaDsgSW50ZWwgTWFjIE9TIFggMTBfMTVfNykgQXBwbGVXZWJLaXQvNTM3LjM2IChLSFRNTCwgbGlrZSBHZWNrbykgQ2hyb21lLzExNC4wLjAuMCBTYWZhcmkvNTM3LjM2
  • Node 环境:dW5kZWZpbmVk

可以看出,同样的代码在不同的环境运行的结果竟然不一样。

在当前大多数环境下,网站的 JS 代码都会经过一定的混淆来加大逆向程度,这样不容易被看出来代码执行逻辑,以上代码我是在 https://www.lddgo.net/encrypt/js 网站上进行了默认的混淆,接下来我们看下原始代码:

js
navigator = {};  
function getParam(){  
    let nav = navigator ? navigator : ''  
    return btoa(nav.userAgent);  
}  
console.log(getParam());

可以看到代码中从 navigator 对象中获取来 userAgent 属性,最终做了一个 base64 编码返回,在 Node 环境中,是没有 BOM 对象,自然就没有了 navigator,所以当取 userAgent 属性是返回 undefined,所以结果自然就不一样了。

所以我们需要补浏览器环境,使得 Node 环境也包含各种各样的 BOM 对象,最终可以像浏览器一样获取 navigator.userAgent 属性,这样就可以与浏览器环境中获取的值一样了。

怎么补浏览器环境?

如何获取所需环境

要想知道需要补哪些浏览器环境,那么就需要知道扣下来的加密算法代码到底使用了哪些浏览器环境的API,然后再对应补上这些环境。

以下列举 2 种如何知道加密算法代码使用了哪些浏览器环境的办法:

  1. 直接把扣下来的代码放进 Node 环境执行,看报错信息,例如 Uncaught ReferenceError: xxxxxx is not defined,然后把 xxxxxx 补到代码最前面即可;
  2. 使用 Proxy 代理来监听,可以通过 Proxy 创建一个对象的代理,从而实现基本操作的拦截和自定义(例如赋值、查找、枚举、函数调用等),它可以代理任何类型的对象,包括原生数组、函数、甚至是另外一个代理对象(无限套娃);

以下是一个 Proxy 代理的使用案例:

js
// 创建一个 navigator 代理对象
const navigatorProxy = new Proxy(navigator, {
  get: function(target, property) {
    console.log('访问属性:', property);

    // 检查 property 是否是一个方法
    if (typeof target[property] === 'function') {
      // 如果是方法,则绑定到 navigator 对象上并返回
      return function (...args) {
        console.log('调用方法:', property);
        return target[property].apply(target, args);
      };
    }

    // 如果不是方法,则返回属性值
    const value = target[property];
    console.log('获取属性值:', value);
    return value;
  },
  set: function(target, property, value) {
    console.log('设置属性值:', property, '=', value);

    // 设置属性值
    target[property] = value;
    return true;
  }
});

// 使用代理对象访问 navigator 的属性和方法
console.log(navigatorProxy.userAgent);  // 输出用户代理信息
navigatorProxy.language = 'en-US';  // 设置语言
console.log(navigatorProxy.language);  // 输出语言

输出结果如下:

访问属性: userAgent
获取属性值: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36
设置属性值: language = en-US
访问属性: language
获取属性值: zh-CN
zh-CN

其实就是做了一个中间人,当执行 navigatorProxy.userAgent 的时候,会进入上述代码中的 get 方法,这里面我们可以写一些我们的逻辑,例如输出日志等,可以很方便的知道加密算法代码都用到哪哪些对象的哪些属性值。

上述代码中的 getset 都被称为 handler,如果还需要拦截其他方法,还有更多的 handler,例如 applyconstructdefinePropertydeletePropertygetOwnPropertyDescriptorgetPrototypeOfhasisExtensibleownKeyspreventExtensionssetPrototypeOf官方文档

还有个问题,上述代码中的 const navigatorProxy = new Proxy ,因为当我们代理对象之后,要通过对新对象操作也就是 navigatorProxy 才会进入到我们的 handler 中,但是在逆向过程中后续的代码肯定还是对 navigator 进行操作,那么能不能把代码改为 navigator = new Proxy 呢? 这个要分情况,因为有些 BOM 属性是只读的,所以代理会失效,这个后面会讲到

编写补环境代码思路

上面讲到了如何获取需要补充的浏览器环境,那么后面我们要如何写代码去补呢?这里也分了几种思路:

  1. 补头(俗称补充头部):把所有需要用到的浏览器环境全部补充在代码最顶部,如果需要用到 BOM 、DOM 对象,就在代码顶部定义空对象,例如 var document = {}document.body = {}
    • 优点: 对于一些检测较少的网站,使用时间最少,因为加密逻辑并不会与 BOM、DOM 耦合度很高,所以只要补上环境不报错就可以得出结果;
    • 缺点:通用性很差,当遇到其他网站时,要根据不同的网站补不同的环境;
  2. 补环境框架(硬编码):用 JS 代码去模拟浏览器中的 BOM、DOM 对象,然后通过原型链讲这些对象全部组织起来,形成一个纯 JS 打造的丐版浏览器环境。
    • 优点: 根据长时间积累环境,对于一些网站具备通杀能力;
    • 缺点: 上手难度高,实现起来比较复杂;
  3. 补环境框架(DOM解析树): 此思路是第 2 种思路的升级版,核心在于关于 DOM 不再采用硬编码,而是使用第三库实现 DOM 解析树。
    • 优点:通杀能力会比第 2 种思路更强
    • 缺点:实现起来会更为复杂一些,并且耗时会久一些;

总结

  1. 通过在 Node 环境中补充 BOM、DOM 对象就是补浏览器环境;
  2. 通过补浏览器环境,相对于扣算法会省事省力,更快的时间得到结果;
  3. 可以通过 Proxy 代理器的方式获取加密算法需要用到的浏览器环境,通过不同思路的方式去补充浏览器环境;

Released under the MIT License.