关于DOM树解析功能,本来想着套用jsdom,但jsDOM太大了,于是看jsdom底层如何实现DOM树解析,发现是使用的parse5,从parse5的相关项目中又知道了cheerio, 于是想着如何套到框架里。后来从蔡老板和风佬的补环境课程里学了一手DOM树解析后,把之前的补环境框架中DOM树解析相关代码更新了下,简化了很多代码。DOM树相关的操作如createElement, appendChild,childNodes也不再是硬编码了,框架也变得通用一些。这就挺好的,虽然执行速度上慢了一些,也不像浏览器的DOM树操作那样完备,但通用性已经大大增强。

于是拿着这个补环境框架锤子到处锤一锤,反正不用细细研究加密逻辑,花不了太多时间。锤rsvmp,把之前的服务升级了一下,简化了下代码。锤某里225,目前的情况是能生成token但过不去,估计补的不对,225主要不是刚需,就先放一放。后续有时间了,还是先锤一下某里的140才行,循序渐进,正好140有哲哥现成的AST还原代码,补不对的时候可以对着代码看一看,而225的还原没有现成的,得自己写AST还原插件。

蔡老板和风佬的补环境课程还是值得一学的。

联系作者

之前因为Node环境容易被检测,就用vm2沙盒来跑,可以避免很多检测。虽然大佬们都说vm2也是可以被检测的,但一直能跑,就一直用着。

最近在搞一个网站,一直过不去,仔细调试后发现是因为VM2_INTERNAL_STATE_DO_NOT_USE_OR_PROGRAM_WILL_FAIL.handleException这个东西。搜索vm2的源码,在transform.js里可以找到如下代码

1
2
3
4
5
6
7
8
9
10
const name = assertType(param, 'Identifier').name;
const cBody = assertType(node.body, 'BlockStatement');
if (cBody.body.length > 0) {
insertions.push({
__proto__: null,
pos: cBody.body[0].start,
order: TO_LEFT,
coder: () => `${name}=${INTERNAL_STATE_NAME}.handleException(${name});`
});
}

类似的代码还有好几次,作为实用主义,就不细细研究vm2为啥要加上这些代码,简单粗暴的把这一部分删掉后就跑起来了。

之前用jsdom, jsdom被检测,现在用vm2, vm2也被检测,对抗之路真是永无止境。看来还是得selenium一把梭才是王道啊,只是selenium如何方便的做成一个服务,工程化问题如何解决呢,这是个问题,要不然selenium自动化真是香喷喷。

联系作者

最近技术群里分享了篇文章《WebRTC泄露源IP的防范措施》,关于WebRTC泄露真实IP,在我对反爬的一些理解中其实也提到过这个问题

对于浏览器环境的检测,有Canvas指纹,Webgl指纹,Audio指纹,字体指纹,还有WebRTC指纹等等。WebRTC可以获取到客户端的实际内外网IP,即便浏览器加了代理。对Canvas指纹,Webgl指纹,Audio指纹,字体指纹做校验是为了防止爬虫用程序批量抓取,因为这种批量抓取,指纹大都是一样的。当然对于有经验的爬虫,这些都防不住,指纹可以修改,WebRTC可以禁用。

一些知名厂商的反爬方案中就有用到WebRTC这个漏洞,如果使用浏览器采集的时候,发现并发上不去,很有可能是因为WebRTC暴露了真实IP。

网上说解决的办法是安装WebRTC Leak Shield等类似插件,在测试网站上https://ip8.com/webrtc-test可以测试是不是生效,加上代理,安装插件后,测试发现确实生效,没有暴露真实IP。但在https://api.ipify.org/网站上测试的时候,还是发现暴露了真实IP,目前还不知道为啥。

联系作者

虽然知道ChatGPT厉害,但一直没有时间去注册个账号玩玩,最近终于有时间了,注册了个账号体验了下,真心不错。

之前群友提了个正则表达式的问题,用正则表达式匹配出现2次及2次以上的字符,这个问题读大学的时候舍友郭老曾经问过我,作为曾经的正则小王子,当时就知道要用反向引用,现在当然也知道。和群友说了下这可得用到正则的高级用法,可以看一下反向引用,一会儿他问了ChatGPT,就给他解答了。现在拿着这个问题问ChatGPT, 得到如下答案。

要匹配出现2次及2次以上的字符,可以使用正则表达式中的花括号 {} 来指定匹配次数。

假设要匹配的字符串为 hellooo worlddd,我们可以使用如下的正则表达式来匹配出现2次及2次以上的字符:
(.)\1{1,}
这个正则表达式的含义是:匹配任意一个字符 (.),然后匹配和它相同的字符至少1次 {1,}。其中 \1 表示反向引用前面匹配的字符,也就是 \1 所在的位置应该与前面 (.) 所匹配的字符相同。

去年初在学习js补环境的时候,想知道在jsdom中如何在解析页面前执行js代码的办法,找了一圈没找到答案。买了猿人学的课,看了所有的js视频,最终找到了解决的办法,也就是beforeParse方法,如今把这个问题丢给ChatGPT, 就能得到答案。

在使用 jsdom 解析 HTML 页面之前,你可以通过设置 beforeParse 选项来在解析之前执行 JavaScript 代码。

具体来说,你可以创建一个 JSDOM 对象并设置 beforeParse 选项为一个回调函数,在这个回调函数中编写需要执行的 JavaScript 代码。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const jsdom = require("jsdom");
const { JSDOM } = jsdom;

const html = "<html><body><div id='myDiv'></div></body></html>";
const options = {
beforeParse(window) {
// 在这里执行需要在解析页面前执行的 JavaScript 代码
const myDiv = window.document.getElementById("myDiv");
myDiv.textContent = "Hello, world!";
}
};

const dom = new JSDOM(html, options);
console.log(dom.window.document.querySelector("#myDiv").textContent);
// Output: "Hello, world!"

在这个例子中,我们创建了一个包含一个空 div 元素的 HTML 页面,并设置了 beforeParse 选项为一个回调函数。在这个回调函数中,我们使用 getElementById 方法获取了这个 div 元素,并将其文本内容设置为了 “Hello, world!”。然后我们创建了一个 JSDOM 对象并传入了这个 HTML 页面和选项对象,最后我们验证了修改后的 div 元素的文本内容是否正确。

注意,beforeParse 回调函数只能在 JSDOM 对象创建时执行一次,无法在解析完成后再次执行。如果你需要在解析完成后执行 JavaScript 代码,可以使用 window.onload 或 window.addEventListener 等方法。

前几天前同事又问了个算法题,写一个函数split_text,参数一个是字符串text 一个是列表 dic,都是英文单词 返回一个列表 每个元素都是一个英文单词,并且顺序连起来,就是text 比如 split_text(‘appledesktop’, [‘a’, ‘app’, ‘apple’, ‘desk’, ‘top’, ‘led’]) 返回 [‘apple’, ‘desk’, ‘top’] split_text(‘appledappleatop’, [‘a’, ‘app’, ‘apple’, ‘desk’, ‘top’, ‘led’]) 返回 [‘app’, ‘led’, ‘apple’, ‘a’, ‘top’]

您可以使用动态规划来解决这个问题。首先,创建一个长度为text长度+1的列表dp,dp[i]表示text[0:i]的分割结果。初始化dp[0]=[]。然后,从i=1开始,遍历dp列表。对于每个i,从j=i-1到j=0倒序遍历。如果text[j:i]在字典dic中,那么将dp[i]更新为dp[j]加上[text[j:i]]。最后返回dp[-1]作为结果即可。

下面是实现这个函数的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
def split_text(text, dic):
dp = [[] for _ in range(len(text) + 1)]
dp[0] = []

for i in range(1, len(dp)):
for j in range(i - 1, -1, -1):
if text[j:i] in dic:
if dp[j]:
dp[i] = dp[j] + [text[j:i]]
break
else:
dp[i] = [text[j:i]]
return dp[-1]

您可以运行一些测试来验证函数是否正常工作:

1
2
3
4
5
6
7
text = 'appledesktop'
dic = ['a', 'app', 'apple', 'desk', 'top', 'led']
print(split_text(text, dic)) # ['apple', 'desk', 'top']

text = 'appledappleatop'
dic = ['a', 'app', 'apple', 'desk', 'top', 'led']
print(split_text(text, dic)) # ['app', 'led', 'apple', 'a', 'top']

问ChatGPT,还有更优的办法吗?它又给出了回答。

上面给出的动态规划方法是一个不错的解决方案,但是也可以使用更简单和更高效的贪心算法来解决这个问题。

具体来说,对于给定的文本字符串text和字典列表dic,我们可以考虑从text的左侧开始遍历,每次查找能够匹配字典中某个单词的最长前缀,并将该单词加入结果列表中。重复这个过程,直到text被完全分割成单词为止。

下面是实现这个贪心算法的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def split_text(text, dic):
result = []
i = 0

while i < len(text):
found = False
for word in dic:
if text.startswith(word, i):
result.append(word)
i += len(word)
found = True
break
if not found:
# 如果在字典中找不到匹配的单词,则返回空列表。
return []
return result

真的是强到离谱,这之后用搜索引擎的频率就低很多了。

联系作者

两年前刚学会AST的时候,就编写AST代码还原过某盾的混淆,但一直没有去尝试生成请求参数,测试接口,因为代码将近5000行,那时只会抠代码,忙于业务,疲于奔命,根本没有时间和精力尝试。但自从学会补环境技术后,效率大大提高,于是打算拿着补环境框架锤一下某盾的加密,顺带完善一下补环境框架。

在此之前和波哥讨论过某盾的设备指纹,波哥说会设备指纹会生成不同的加密版本,但其实逆向届有句名言,一个动态的事物,找到动态点之后,动态就会坍缩成静态。所以不管你怎么变,找到总有不变的地方,找到不变的地方,就好办了。在这里,不管你生成多少加密版本,你的检测点一般是不会变的,只要过了这些检测点,加密参数自然能通过校验。

拿到混淆代码,用AST技术还原控制流,再还原字符串,代码好懂很多。只能说这个混淆强度目前已经太容易了,两年前就能还原,现在更不在话下。这两年,AST技术经过蔡老板的推动,控制流混淆还原技术已经炉火纯青,除非加入vmp混淆,要不然一般控制流根本挡不住。

把还原后的代码放到补环境中跑起来,发现代码里使用了Promise异步, 这还是第一次遇到。于是去看看Promise的用法,再看看安佬的课件,再和大佬们讨论某盾后,知道了Promise是怎么一回事,也有了大致的方案。于是尝试生成签名,发现生成一直不正确,主要原因还是没法很好的处理Promise,短时间内估计以自己的能力找不到很好解决的办法,只好下场改加密代码,最终可以通过代码生成请求参数,调用接口取得tokenId。这就违背了补环境技术不改加密代码的原则,但因为平时也遇不到Promise,就先这么处理。

问过同事,这个接口其实作用不大,只有过了业务校验接口,才算真正的通过。那就此打住,不再进行下一步的尝试,意义不大。

这波测试下来,主要就以下几个问题

  1. 一是代码的混淆强度不够,这种两年前就能搞定的混淆强度,放到现在根本不堪一击,在蔡老板的推动下,AST还原技术已经快人手必备了。必须得上vmp才行,vmp是真难搞,能力不强真还原不了。
  2. 二是代码还是静态的,固定一份代码就行,一直能用,这就好搞很多了。所以还是得动态变化才行,让人没法固定。
  3. 三是检测点不够多,还得针对补环境技术增加一些检测点才行

希望还在某盾做混淆的同事如果看到这篇文章,可以根据以上几点意见改进一下,等改进后再来测试一下。

联系作者

在搞定rs4后,补环境技术成了我手中的锤子,我直接化身西北锤王,见到一个网站就想用补环境去锤一下,很快就搞好了rs5,接下来就要搞定rs6, 但因为rs6是vmp, 所以在这之前,得先搞点简单的vmp热热身,toutiao是个不错的选择。

关于vmp,可以看看Nanda的VM防护介绍及企鹅滑块分析, 关于vmp打日志,可以看看卷木木的新版某数分析思路

完成rs5后,除了dom树操作,环境已经相对比较完善了,再来搞toutiao, 就会发现简单很多。相比较rs5, toutiao主要增加了对WebGL对象的校验,以及Plugins的校验。其中WebGL缺啥补啥就完事了,而Plugins的补充就麻烦很多,好在肝总大佬已经有现成的代码,直接拿来用就行了,真是省心又省力。

打上日志,toutiao的vmp就只执行了3000多次,而rs6执行了2万多次才生成cookies,规模上的差距还是很大的,但道理是相通的。搞定toutiao后再来搞rs6的vmp就轻松很多了,唯一困难的是rs6的dom树操作,要花些时间了,大佬们建议自己去实现dom树,还在想办法看看怎么实现中。

整体来说,toutiao用来做vmp入门是极好的,大佬们已经不是停留在补环境了,直接把加密算法都给还原了,真是人外有人。

联系作者

太长不看版,开源的node-rate-limiter-flexible插件可以用作Node服务限流,非常好用。

最近把之前提供给业务方使用的Node服务对接到公司的中台服务,用上了服务限流,真的很香。本以为像我这样的非主流程序员一辈子都不会用上服务限流,没想到现在就用上了。

这个Node服务之前是通过运维提供的Nginx转发给业务方使用, 没有提供限流功能。一旦调用方的并发提高很多,导致负载过高,调用方会因为等待时间过长,导致调用超时。而健康检测也一样调用超时,只要有一个节点健康检测失败,就将流量打向其它的节点,其它节点流量就变高,之后调用超时,健康检测失败,又将流量打向其它节点,整个服务直接雪崩。

换成中台envoy服务转发后,就没有超时了,QPS也提高了,莫名其妙,运维也不给你机会找出原因,就此作罢。用中台的服务时,配置限流是用的令牌桶算法,提供了3个参数给你配置,最大桶数量,生成桶时间间隔,每次间隔生成桶数量。其实就是令牌桶算法,就是配置最大令牌桶和令牌生成速率,其中最大令牌桶用来支持突发流量。看了下令牌桶算法,就知道是怎么一回事了。假设QPS是300,超时时间5s,那么设置的最大令牌桶可以是1500,超过1500,有一些请求就会超时了,就这么简单。在测试环境测试后,看这限流生效了,就配置正式环境服务限流。

一切都还好,知道有一天,我发现请求的QPS超过了我设置的QPS,并且都请求成功了,也就是限流没有成功,于是找到中台服务提供方,那他们给个解释。折腾老半天后,才发现,中台的envoy服务没有做服务端限流,只做了客户端限流,服务端限流不知道啥时候才会安排。而我之前在测试环境的测试,只是在客户端限流。这就真的离谱,哪里有限流不做服务端限流的,真的离谱到家了。

于是又想用运维提供的Nginx做限流,让运维增加限流配置,测试了一圈发现提供的Nginx配置是针对客户端的IP限流,不是我想要的服务端限流,而因为之前运维的Nginx转发老是导致我的服务不可用,所以心理上也不太想用他们提供的转发,于是只好自己实现服务端限流。要实现整体服务限流,需要引入redis之类了额外服务,所以只想在每个服务节点内部实现限流。因为是Node程序,开源的node-rate-limiter-flexible插件就可以用来实现这个功能,其中BurstyRateLimiter支持突发流量,于是用它。它是用了两个限流配置,第一个的令牌用完了,就会去用第二个的。如下就是一个QPS是4,支持突发流量到8的配置,相当好用。第一个配置每一秒生成3个令牌,第二个配置每5秒生成5个令牌,那么5秒内就一共可以生成3 * 5 + 5 = 20个令牌,相当于这5秒内的QPS是4,而它的突发流量是8,也就是第一秒内如果进来8个请求,它也不会触发限流。

1
2
3
4
5
6
7
8
9
10
11
const burstyLimiter = new BurstyRateLimiter(
new RateLimiterMemory({
points: 3,
duration: 1,
}),
new RateLimiterMemory({
keyPrefix: 'burst',
points: 5,
duration: 5,
})
);

联系作者

和rs对抗两年多了,作为国内js逆向的天花板之一,是许多js逆向的衣食父母,我这口饭也是rs给的,真心感谢rs公司,给我们带来了这么优秀的反爬产品,让自己的反爬和反反爬水平都得到了提升。

两年前,刚接触js逆向时,接手同事的项目就是rs4天花板,那时候能把人累死。那时还不会AST, 同事也不会,在遇到控制流的时候,为了理解代码逻辑,都是一行一行的,然后记录关键步骤。每个参数的生成,如cookie, url后缀,指纹等等都得记录好多行。不到一个月后,同事离职,最终我扛下了所有,那时真是初生牛犊不怕虎。

一切都还好,直到4代升级到5代, 没有想到用4代那种正则抽取参数的方式,程序挂了,因为之前4代是用正则抽取的,更新到5代之后没法直接用了。单步调试也没有之前方便,不好打条件断点,再用最原始的单步调试就显得很吃力。于是开始想办法,正好同事从蔡老板那了解到AST,并做了一次分享,于是开始学AST,在蔡老板的星球里看到同事做过的一道题目,瞬间悟了,这不就是二叉树操作吗。用了两天的时间把控制流还原好,开始愉快的调试,并可以用AST的办法把参数抽取出来,虽然性能上差了些,但终归能用。

虽然能调试了,但还是卡在了鼠标和键盘这些行为轨迹的生成上,这将近1000行的代码要扣下来无从下手。扣出来的代码也过不了,总是返回400,那段时间是最头疼的时候,每次早上站会的时候,都没什么进展,就很尴尬。后来一个同事来一起帮忙排查问题,同事测试了很多方案,后来测试sekiro的时候,终于200了。这时我突然就明白了,服务器能拿到的就一个字符串,它其实不知道你是咋生成的。在rs中,轨迹这些最终会转成一个数组,不一定要用js代码生成,直接拿浏览器中的轨迹数组随机变化下生成一些数组也可以。经过修改后,代码又开始跑起来了。

程序跑到21年2月,通过率越来越低,抓取速度也越来越慢,最终行为轨迹又升级了,彻底跑不动。这时开始混圈子,认识了哲哥和涛哥,在他们的鼓励下,硬着头皮把1000行的代码扣下来,程序又开始跑起。又经过2到3个月的对抗,经历了20多次小更新,网站的反爬越来越完善,校验越来越完备,自己对于反爬和反反爬也有了理解,水平也得到了哲哥和涛哥的认可。

今年知道了补环境,于是尝试使用补环境来完成, 在完成了zptoken的补环境框架后,普通rs4的补环境变得相当简单, 除了dom树操作遇到问题。想想自己学逆向是先抠算法,再学AST,再学补环境,这条路是走错了。更简单的路径应该是先学补环境,再学AST,之后遇到性能问题的时候才去抠算法。

联系作者

当补环境遇到document.all

在解决jsdom被针对的逆向过程时遇到了这个问题, document.all的类型是undefined, 但却能取到值,js这是真的离谱,当时就直接懵了。脑海中回忆这些年学到的JavaScript知识,怎么也想不到解决的办法,于是问各路大佬,其中当然有猿人学的安澜。今年报了猿人学的课,看了安佬讲的js课后,对js有了新的理解,不再纠结于扣算法。安佬说过js逆向除了扣算法,还有很多方向可以尝试,思路得打开。对自动化也不再那么排斥,当看到安佬的jsdom用法后,大彻大悟,用了一个多星期的时间就搞定了通杀方案,从此再也不用一个个网站的干苦力活。虽然之前哲哥和涛哥和我说过jsdom,自己也用了jsdom,但用的不好。从此之后,遇到混淆难的网站,都是先掏出jsdom试试,真的很香。后来遇到难题了,都会先问问安佬。

当拿着document.all去问安佬时,安佬也是第一次遇到这个问题,思考后给出的回复是,在纯js层面解决不了这个问题,得借助其它办法。V佬这次挖的坑是真的深,这明显就是冲着Node环境来了,只要是Node补环境,都会遇到这个问题,一抓一个准。好在山外有山,人外有人,办法总比困难多。安佬问遍各路大佬,document.all这个检测也在圈子里传开了,风佬专门开了次分享,最终风佬给出的办法是得修改Node才能完美的解决,当然风佬也提过一嘴,V8的不可检测对象也可以解决这个问题,如此看来风佬的知识储备是真的强。最近他和蔡老板开了补环境的课,值得学习,在我看来,光蔡老板的AST还原其实已经值这门课的学费,更不用说加上风佬的补环境。只是自己真的是没时间学这些,这些年报了3门课,夜幕的高级课,肉丝的VIP,猿人学,其中夜幕的高级课就打开过一次,肉丝的课就看了下怎么root, 猿人学就看了js部分,真是没时间学,忙于业务,疲于奔命。而另一位大佬肝总也给出了自己的解决办法,肝总是真的强,还乐于分享,真是吾辈楷模。

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

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

联系作者

最近rs上线了针对jsdom的检测,是时候对是否继续使用jsdom的做出决定了。因为有jsdom的存在,直接进入了人均rs的年代,rs公司针对jsdom进行检测也是非常正常的。之前认为在jsdom上做二次开发是个不错的选择,现在看来还是抛弃幻想,趁早放弃吧。

随便翻开jsdom,就发现各种私有变量,多看一下jsdom源码就能发现更多检测点,真是随便检测下都能区分出来。更不用说很多人jsdom用的不对,连userAgent都没换,使用jsdom,就变成了直接往枪口上撞,一抓一个准。

如果非得用jsdom,就只能悄咪咪的用,别声张了。但你得时刻提防着被检测,因为jsdom可以检测的点真的是太多了。

傻瓜化使用jsdom的年代过去了,真要补环境,还是搞一套自己的补环境框架更趁手些。不得不说,js逆向真的越来越难,门槛也越来越高,这何时是个头啊,还得是selenium一把梭香。但说实在话,用selenium,不懂得逆向也是解决不了问题,还得是能看得懂代码才行,因为你总得知道怎么被反爬的。

联系作者