未雨绸缪,虽然有了补环境通用方案,但为了应对可能更高的QPS需求,一直在想办法储备更快的cookies生成方案。简单计算下,假设需要400 QPS,而一般的补环境耗时250ms, 那么一个CPU一秒可以处理4个请求,400 QPS就需要100核CPU, 这对资源的消耗就很可观了。如果有个方案能50ms生成一个cookies, 那么就只需要20核CPU就能满足需求。

寻寻觅觅,在如画佬的星球里找到一个补环境方案,环境代码极少。示例代码是在Python中使用execjs来执行生成cookies, 改成Node服务后执行速度非常理想,能做到80ms生成一次。

美中不足的是需要在Node中使用eval执行js代码,会导致内存泄漏,这种内存泄漏使用强制内存回收也回收不了,最后程序会奔溃,用PM2限制内存使用和自动重启可以解决。但不可避免会出现502服务器错误,这在我们公司的生成环境中就不合适,因为运维一旦发现502超过一定数量就得让解决,得寻求其它方案。

要想又快又稳,想来只有算法方案了。

联系作者

之前在群里就看到有道友在讨论这个问题,但因为没有遇到过这个问题,就没细看,这次自己也遇到了,就认真看了下。

在寻找高效的某数 cookies 生成方案时,看到如画佬星球的补环境方案后,深有感触。最近两年,虽然把补环境框架搞的越来越完善,但同时执行速度也越来越慢,想想似乎有点背离补环境的初衷。上一次yidun的加密生成方案尤其明显,补环境方案把整个CPU都打满了,后面搞了个补环境和扣代码结合的方案后跑的就稳稳的,两者之间性能整整差了10倍。

于是就尝试这种最开始学习的补环境方案,缺啥补啥,争取补的环境每一行代码都是有用的,然后就遇到这个 ActiveXObject 问题了。在 proxy 输出日志中,看到有 ActiveXObject 调用日志,于是在浏览器里看,window.ActiveXObject 就是 undefined, 在Node里,window=global 后,window.ActiveXObject 也是 undefined,于是就没管 ActiveXObject 。但生成的 cookies 就是过不去,只好和如画佬的代码做对比,发现就是因为他的代码有 window.ActiveXObject=undefined,所以能过。

于是去了解 ActiveXObject,发现它是IE才有的对象,在IE中,’ActiveXObject’ in window 是true, 而在 Chrome 中 ‘ActiveXObject’ in window 是 false。如果在检测代码中有 ‘ActiveXObject’ in window 这种检测,那么补环境代码中有没有 window.ActiveXObject=undefined 就会影响检测代码执行路径了。在Node中加上 window.ActiveXObject=undefined 后,’ActiveXObject’ in window 也就成了 true 了,检测代码就会认为是 IE浏览器,就会按照IE浏览器来检测。手头上方便调试的只有Chome浏览器,就再继续探究后面如何检测的。神奇的是,我是在Mac上用Chrome调试的代码,最后还得伪装成IE才能通过检测,就离谱。

回过头来看代理日志,有针对in操作符的代理has=> [window] 有无属性名 => [ActiveXObject], 结果 => [false] 这行,就是 代理has输出日志,说明检测代码里有 ‘ActiveXObject’ in window 的检测,只是没注意到。

联系作者

本来是想问ChatGPT在Node.js里如何读取Redis的,问着问着,就拿如何解决document.all问题去问ChatGPT, 顺便测试下其它几个厂商给出的回答,其它几个都是挺常规的回答,只有Copilot给出的回答出乎意料,因为它的回答是基于我的当补环境遇到document.all文章给的。

Copilot 给的回答是

在解决 jsdom 被针对的逆向过程中,遇到了 document.all 的问题。尽管 document.all 的类型是 undefined,但却能够获取值,这让人感到非常困惑。不过,我们有一些解决办法:

  1. 不补环境:有时候网站的校验可能不严格,直接绕过补环境步骤也许就能成功。
  2. 扣算法:这是一种绕过各种检测的方法,直接将值写死。不过,这需要一定的技巧。
  3. 使用 V8 中修改后的不可检测对象:这是风佬提出的方法,可以完美解决问题。1
  4. 请大佬们出山,共同探讨解决方案。
    总之,解决这个问题的途径有很多,我们可以根据具体情况选择合适的方法。

我的文章里说到:

目前看来,解决这个问题的办法有如下几种

  1. 直接不补了,有可能网站校验不严格,直接可以过
  2. 扣算法,这样绕过各种检测,直接把值写死,就是扣算法难搞。
  3. 用风佬修改后的Node
  4. V8里的不可检测对象
  5. 请大佬出山帮你解决

惊不惊喜,意不意外。阿龙的学习笔记作为个人学习笔记从13年就开始写,有一年还因为域名续费失败的原因,导致域名被抢注而换到现在的域名,但还是坚持独立博客写下来,不管有没有人看。最近一份工作的主要工作内容是Web逆向,写了很多补环境相关的技术文章后收到一些道友的关注,并加联系方式讨论相关问题,还收到一位不知名字道友的20块钱巨赏, 甚是鼓舞。今天看到自己的文章被AI引用,甚是开心。

还是那句话,兼济天下则达,独善其身则穷。

联系作者

很多道友使用jsdom来补环境,但没有设置好userAgent(以下简称ua), 如果生成的token中有使用到ua, 反爬人员就可以通过分析token发现是由jsdom生成的,于是针对jsdom的特征做检测。所以使用jsdom时,设置ua很重要。

下面我们来看看怎么设置ua, 网上常见的设置ua代码如下

1
2
3
4
5
6
7
8
9
10
const jsdom = require("jsdom");
const { JSDOM } = jsdom;
const userAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
const dom = new JSDOM(``, {
url: "https://example.org/",
referrer: "https://example.com/",
userAgent: userAgent,
});
window = dom.window
console.log(window.navigator.userAgent)

输出结果为 Mozilla/5.0 (darwin) AppleWebKit/537.36 (KHTML, like Gecko) jsdom/24.0.0, 有jsdom特征,与设置的ua不一致。

尝试添加如下代码

1
2
window.navigator.userAgent = userAgent
console.log(window.navigator.userAgent)

输出结果依然为 Mozilla/5.0 (darwin) AppleWebKit/537.36 (KHTML, like Gecko) jsdom/24.0.0

继续尝试增加如下代码

1
2
3
4
navigator = {
userAgent: userAgent
}
console.log(window.navigator.userAgent)

输出结果依然为 Mozilla/5.0 (darwin) AppleWebKit/537.36 (KHTML, like Gecko) jsdom/24.0.0

查看官方文档,使用ResourceLoader可以修改ua, 代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
const jsdom = require("jsdom");  // 引入 jsdom
const { JSDOM } = jsdom; // 引出 JSDOM 类, 等同于 JSDOM = jsdom.JSDOM
const userAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
const resourceLoader = new jsdom.ResourceLoader({
userAgent: userAgent
});
const dom = new JSDOM(``, {
url: "https://example.org/",
referrer: "https://example.com/",
resources: resourceLoader,
});
window = dom.window
console.log(window.navigator.userAgent)

输出结果为 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, 已经没有jsdom的特征,目标达成。

联系作者

在使用Express搭建Node服务时,会遇到内存持续增长的情况,增长到一定程度后,进程就会被PM2强制重启。而如果进程正好重启时,接收了请求,这个请求就会没有响应,测试就会返回一个502给请求方。这种情况502情况一旦多起来,就会有告警,服务的可用性也就没法做到99.99%。

此时去排查为啥内存会持续增长,看看是不是哪里存在内存泄漏。排查下来后发现,并没有存在内存泄漏的情况,单纯是因为内存的释放速度赶不上内存的占用速度,如果并发不是很高的情况下,内存一直可以稳定释放,而一旦并发高了,内存就来不及释放,就会持续增长。此时只好强制开启内存释放,调用V8中的强制回收内存函数,实现内存的强制回收,使内存一直稳定在健康的状态。

具体如何操作,参考开始佬的编译nodejs c插件-纯笔记一文

当然,鱼和熊掌不可兼得,使用强制内存回收是个很耗时的操作,对请求响应时间会有影响,例如请求20次之后强制内存回收,平均请求时间会慢20%。

联系作者

今年如约开启了海外反爬之旅,对常见的四个海外反爬中inCaplusa,Akamai, CloudFlare(一般称为5秒盾), reCaptcha粗略评估后,觉得inCaplusa最为简单,决定先挑软柿子捏,于是先拿inCaplusa试试水。

打开网站,会先返回一段会执行eval的js, 用于生成___utmvc这个cookie, 这段js是个ob混淆,此时祭出蔡老板的ob解混淆工具搞一搞就好了,不难。很多网站没有这个cookie也没关系,可以先不看。

之后是get请求一份网站对应的版本文件,然后会post请求一次版本文件相同的url, 其中返回的token就是reese84。需要说明的是,即便inCaplusa觉得有异常,也会返回这个token, 也就是说token不一定有效,得拿去请求了才知道对不对。一般拿去请求时,状态码不是403就代表reese84是有效的。

拿到版本文件,先进行AST还原,然后开始补环境。不得不说,这个环境校验是真的细,常规的如Canvas, WebGL, font, window[“Function”][“prototype”][“toString”]‘toString’, window[“Function”][“prototype”][“call”]‘toString’等检测。还有第一次见的类似window[“Object”]“getOwnPropertyNames”的检测, 这个得能做到只返回[‘length’, ‘name’]。还有iframe里的contentWindow检测,这里就不一一列举了。

好在代码结构很清晰,可以一步一步调试,补到一半补累了,于是改成扣代码了。花了一天时间扣了下代码能通过之后,信心大增。拿着这个网站的代码去测试其它网站,发现过不去,于是继续补环境,没有全部补完就能过了,估计是校验不严格。测试了几个其它的网站,都能通过,美滋滋了。

目前只是测试了reese84有效性,后续风控之类的还没涉及到。总的来说,如果不是校验不严格,补环境挺不容易的,太多细节了,用来完善补环境框架再好不过了。

联系作者

上周写完使用Canvas指纹插件被检测到后,Nanda佬正好看到,提了一句关注下HTMLCanvasElement.prototype.toDataURL.prototype,这个值可能发生变化。

于是对比原始浏览器和重定义toDataURL函数后的 toDataURL ,果然不一样。原先浏览器的 HTMLCanvasElement.prototype.toDataURL.prototype 的值是 undefined , 重定义toDataURL函数后,HTMLCanvasElement.prototype.toDataURL.prototype 的值就变成了函数。加上HTMLCanvasElement.prototype.toDataURL.prototype = undefined 后,网站上就返回了边界数据,也不返回假数据了。

继续测试了几个 toString 后有 native code 字样的函数,如 toString, atob, setTimeout 等等,ChatGPT 的解释是有 native code 意味着该函数的具体实现是由浏览器或 JavaScript 引擎提供的,而不是由 JavaScript 本身的代码编写的,它们的 prototype 都是 undefined 。

这就值得引起注意了,在补环境的时候,对native函数都要引起重视,重定义 native 函数后,要保证它们的 prototype 还是 undefined。打开自己写的补环境框架,测试了几个 native 函数,prototype 都是函数,处理的真垃圾。打开开始佬开源的 qxvm, 测试了几个 native 函数, prototype 都是 undefined,处理的真不错。

联系作者

自从知道了网站会采集浏览器设备指纹和WebRTC泄漏真实IP后,在浏览器上常年挂着指纹对抗插件,Canvas Fingerprint Defender,Font Fingerprint Defender,WebGL Fingerprint Defender,WebRTC Network Limiter。突然某天发现自己访问某地图网站时返回了假数据(假数据的形式是不返回围栏边界数据,地址可能出现错误),在排查问题后,锁定了Canvas Fingerprint Defender插件和WebGL Fingerprint Defender插件,只要打开这两个插件中的任意一个都会返回假数据。

于是排查Canvas Fingerprint Defender插件,查看这个插件的代码,发现主要是hook了toBlob,toDataURL,getImageData方法。于是尝试自己写了一个hook toDataURL方法,用了V佬的toString保护函数,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
(function() {
//'use strict';
console.log('start hook')
var v_saf;
!function(){var n=Function.toString,t=[],i=[],o=[].indexOf.bind(t),e=[].push.bind(t),r=[].push.bind(i);function u(n,t){return-1==o(n)&&(e(n),r(`function ${t||n.name||""}() { [native code] }`)),n}Object.defineProperty(Function.prototype,"toString",{enumerable:!1,configurable:!0,writable:!0,value:function(){return"function"==typeof this&&i[o(this)]||n.call(this)}}),u(Function.prototype.toString,"toString"),v_saf=u}();

var v_new_toggle = false;
var v_console_logger = console.log
var v_console_log = function(){if (!v_new_toggle){ v_console_logger.apply(this, arguments) }}

const myToDataURL = HTMLCanvasElement.prototype.toDataURL;
Object.defineProperties(HTMLCanvasElement.prototype, {
toDataURL: {
value: v_saf(function toDataURL(){
v_console_log("[*] HTMLCanvasElement -> toDataURL[func]", [].slice.call(arguments));
debugger;
return myToDataURL.apply(this, arguments)
})
},
})
// Your code here...
})();

把代码放在油猴里跑,神奇的是,即便在toDataURL啥都没有做,toDataURL生成的结果和修改之前一模一样,依然能被检测到。使用Object.getOwnPropertyDescriptor(HTMLCanvasElement.prototype, ‘toDataURL’)和HTMLCanvasElement.prototype.toDataURL.toString()查看,和未使用hook之前几乎长的一模一样。这就超过了我所学的JavaScript知识了,涉及到知识盲区了。感兴趣的大佬可以试试,解决了的话可以告诉我一声。

一时半会找不到问题所在,于是尝试下载Chromium,照着网上随机Canvas画布的文章, 自己编译了个Chromium, 在browserleaks.com上测试了下,Canvas指纹可以随机了,访问网站也没有给假数据。

联系作者

之前用补环境框架测试过某yd验证码,虽然能过,但存在一点不足,那就是速度太慢了,生成一次参数将近1秒,时间都花在创建沙盒环境和执行无效代码了。这么慢的速度,放在生成环境的话,得需要花费很多CPU资源,于是得想想其它的方案。

最先想到的是扣代码,把参数加密相关的函数缺啥补啥的一个一个拿出来,在使用补环境之前试过这个方法,麻烦是麻烦一些,但总能解决。后来和时光佬提起这个事情,他在时光漫漫星球里写过相关的文章,可以去参考下。在时光佬的帮助下,很快就搞定cb, fp, 轨迹加密等参数,很快就跑起来了。测试了下速度,能做到100ms生成一次参数,是之前补环境的10倍,这速度满足需求了。

奇葩的是,每小时的0到20分钟过不了校验接口,而每小时的20到60分钟就能过去,但浏览器上是任何时候都能过,于是怀疑是b和d接口没请求。于是参考十一姐写的相关文章,把b和d接口加上。不得不说,十一姐写的文章是真的详细,主打一个手把手教程。神奇的是,把b和d接口加上后,还是过不去,这就很离谱了,百思不得其解。后来问了道上的朋友,他们也有类似的遭遇,一直解决不了,既然大家都一样,那就暂时先放一放了。

补环境虽然解决的快,但生成参数的速度真的是太慢了,以后得慎重使用。

联系作者

最近某里滑块更新到227,导致之前的226代码过不去了,于是得排查下原因。在这之前,想着先把混淆代码还原下,这样更方便调试。

之前虽然还原过221,但现在已经更新到227了,得花时间写很多插件,好在可以站在巨人的肩膀,蔡老板的226还原文章就是一个很好的参考,蔡老板的星球里有很多现成的插件,直接拿来用就完事了。

还原的过程中遇到两个问题,一个是自执行函数作用域的问题,另一个是假节点。秋裤佬写过还原出现作用域异常的解决方案,自执行函数作用域问题迎刃而解。假节点就是条件语句里会有很多数学计算,但其实值永远为true,仔细观察它们的特征后写插件解决即可。每次执行完一个插件后,都替换原文件然后在网站上测试代码是否正常,保证还原的准确性。最终的效果是核心代码合并后只有一个case。还有一些字符串常量没有还原,得花不少时间写,等后续有时间了再写插件还原。

代码还原后,我们就可以调试了。先把代码放到补环境框架中跑一跑,token能生成,但过不去,于是只能慢慢调试,这次调试才发现某里滑块混淆代码的厉害之处,它会根据传进来的参数执行不同的流程,调试起来依然很费劲,不愧是国内混淆js的天花板。对比核心数组和浏览器之间的差异,慢慢调试,最后发现增加FocusEvent, 然后第13位固定写死就能跑通了。这次更新和226相比核心代码几乎没变,只是数组位置变了下,然后轨迹校验变严格了,问题不大。

联系作者