本来是想问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相比核心代码几乎没变,只是数组位置变了下,然后轨迹校验变严格了,问题不大。

联系作者

APP抓包入门中,我们有说过有一些复杂的APP,抓包软件是抓不到HTTPS包,这其中的原因有很多,比较常见的如SSL Pinning,还有双向认证,还有一些情况是不走HTTP协议。这里我们来解决最常见的SSL Pinning问题。

SSL pinning简单来说就是证书绑定,SSL证书绑定,即客户端内置了服务端真正的公钥证书。在 HTTPS 请求时,服务端发给客户端的公钥证书必须与客户端内置的公钥证书一致,这样请求才会成功。而使用抓包软件后,抓包软件的公钥证书和客户端内置的公钥证书不一致,这样请求就会失败。

网上有很多解决办法,其中如DroidSSLUnpinning, r0capture, 这里我们使用DroidSSLUnpinning来解决这个问题。

查看DroidSSLUnpinning的使用方法,进入ObjectionUnpinningPlus目录,使用hooks.js脚本即可。需要安装frida使用。frida脚本使用方法有两种,一种attach, 一种spawn。com.example.mennomorsink.webviewtest2 是应用包名,可使用frida-ps -U命令进行查找报名。也可以使用jadx反编译工具,在资源文件AndroidManifest.xml中的第一行,找到package字段即可。有一些frida版本会提示–no-pause无法使用,去掉这个参数即可。

  • 使用方法1 attach : frida -U com.example.mennomorsink.webviewtest2 –no-pause -l hooks.js
  • 使用方法2 spawn : frida -U -f com.example.mennomorsink.webviewtest2 -l hooks.js –no-pause

至于r0capture的使用,可以查看r0capture仓库里的使用文档。这个库功能更强,但要配合WireShark使用。

联系作者

之前有段时间,Node服务的可用性从99.99%跌到了99.3%,502错误明显增加,于是得排查下原因。

在本地打包镜像,启动容器后,把并发打上去,发现PM2守护的进程会时不时重启, 日志如下

1
2
3
PM2      | App [server:1] exited with code [0] via signal [SIGKILL]
PM2 | App [server:1] starting in -cluster mode-
PM2 | App [server:1] online

仔细观察后发现是内存到达上限后会把进程杀死,重启。估计是内存泄漏的原因,一时半会找不到原因,于是只好祭出开始佬写的内存回收插件,加上插件之后内存急剧下降,服务稳如泰山。带来的副作用是内存GC存在时间开销,整体响应时间比原先的服务慢20%,这就得做个取舍。

把插件发布到测试环境后,再次测试,一样稳如狗,于是发布到线上环境,结果还是存在很多502错误,想不到错在哪里。联系网关服务的开发一起排查。网关服务的开发推测是keepalive时间设置的原因。因为请求先到网关,再到服务中,而网关设置的keepalive大于服务设置的keepalive时间时,就会出现网络连接不可用的情况,于是就会认为服务502。

在Express中keepalive时间默认是5秒,改一下keepalive时间即可,配置如下。

1
2
const server = app.listen(port);
server.keepAliveTimeout = 70000;

最终服务可用性达到99.99%,达到目标。

联系作者