vaptcha的验证页面可能为弹出的一个单独标签页,为了方便调试,可以设置一下Chrome的DevTools

在类似如下的验证页面加载过程中,会生成VM环境并加载验证逻辑

VM的执行入口为

其VM代码就是vaptcha-sdk接口返回的内容

返回的内容是混淆过的,可以适当使用AST还原并替换一下。
除了单步调试定位外,还可以通过具体要分析的接口的Initiator中的Request call stack定位,如获取图片的接口

通过对请求流程的分析,可知大致的关键链路
接下来就是对链路中部分关键的请求体参数做详细的分析
首先看一下请求体中的关键参数

多次请求可以发现vi是固定的,应为网站标识。
然后根据刚才的链路分析或者搜一下k参数的值可知k参数就是config接口返回的

对应其中的knock参数(这里是不同的两次请求,故图中的值不一样)
接下来重点分析一下en参数的生成。
先跟着调用栈进去看一下,找到请求体

_0x45bdb3["data"]就是完整的请求体

可以看到_0x45bdb3就是当前函数的入参
跟着调用栈回溯看下_0x45bdb3是怎么生成的

能看到_0x45bdb3就是在这里组成的对象,其中data的值就等于_0x26b8e6,继续跟

_0x1e0f55就是data的值,往上面看还能看到en就是对应_0xad7777,至此就可以专注分析en值的生成了。
跟一下en,也就是_0xad7777的生成逻辑

_0xad7777的生成和_0x23221c有关,而_0x23221c也是由一堆其他变量相加得到的
先来挨个分析这些变量的值是怎么生成的,最后再用encryFunc处理一下就行了。
搜一下变量名,找到赋值的地方

其等于_0x354394["GenerateFP"](),看下这个函数是干啥的(根据函数名猜测和指纹有关)
可以看到其中实现和Canvas指纹相关,看下getComplexCanvasFingerprint这个函数
结合extractCRC32FromBase64函数,就是通过Canvas生成一段具有特征的base64,在进行加密处理生成最终的指纹值,其中Canvas绘图时还会根据入参_0xb1e678来生成不同的文本
不同设备(环境)的Canvas指纹一般是唯一的,在结合具体网站应用上,使用当前浏览器生成的指纹,或用node、playwright、python相关库之类的环境去模拟生成指纹去做请求,大多数情况下都是不稳定的甚至是完全没法用的,如何批量稳定的绕过这里暂且不讨论了
这里的extractCRC32FromBase64函数可以整体扣下来或转换为Python代码。
搜一下变量名,找到赋值的地方

可以看到其等于_0x23367b["sent"](),跟一下
之后遇到的处理流程中涉及sent的en参数的也和接下来要分析的流程类似,故只以当前流程为例进行详细分析
其实现的位置为
正常情况下应该是能走到sent中return语句,而sent中只有简单的取值,也就是说在调用sent之前,即走到case 1块之前,_0x1df4cd[1]的值就已经生成了,那先看一下这个东西是在哪赋值的
搜一下这个变量名,可以发现对其的赋值操作主要集中在_0x1e72d1这个函数中

简单分析一下这个函数的处理逻辑,_0x1df4cd的值可能和_0x49bd55有关
该函数主要围绕_0x49bd55这个入参,同时也会走下面的逻辑重新赋值
这里的函数调用内容是会发生变化的,暂时只用关注当前_0xb049bd这个en参数的流程
可以刷新一下重新跟一下getImage流程,看下_0x49bd55或_0x1df4cd怎么来的

可以看到getImage流程对应的函数返回的是经_0x52fdcb函数处理的一个值,稍微看一下这个函数可知最后它返回的是一个Promise对象
然后_0x52fdcb的第四个函数参数返回的是一个经_0x5513de函数处理的值

可以看到该函数返回的是一个key指向不同函数执行逻辑的对象,每个执行逻辑都和_0x1e72d1函数有关
继续跟(或者结合这些流程稍微梳理一下),就能看到 _0x41c1ce["call"](_0x160280, _0x4bd611)的返回值,即_0x49bd55是_0x5513de函数的第二个函数参数中case 0块的返回值,即对应上述分析的第一个en参数的的处理逻辑所在的块
看一下_0x298102["GenerateFP"](false, true)

其返回的是一个Promise,可以在Promise包裹中的函数下几个断点看看其中的处理逻辑及返回值,将该Promise记为Promise0
return后回到_0x1e72d1函数的处理流程中,由于返回的列表第一个值是4,故走下面的逻辑

继续跟

根据上述对_0x5513de的简单分析,这里走了next的执行逻辑,可以看到上面_0x1e72d1的返回值就是_0x4234aa函数的入参_0x4921b0,可以发现其主要围绕Promise的执行,并逐层传递Promise执行中产生的参数
_0x4234aa函数也是在一个Promise内,而上述提到的getImage对应的函数返回时也正是调用了返回这个Promise对象的_0x52fdcb函数

如果done被标记为true,则会取value给到_0x12a155,即resolve,否则会一直执行then,fulfilled时执行_0x910b2a,其参数简单来说就是Promise执行过程中resolve接收的值,reject时执行_0x447f35,这里主要关注Promise的onFulfilled函数,即_0x910b2a,逻辑如下

分析可知这里的next最终还是要回去执行刚才分析的_0x1e72d1函数,来标记done的。这里的_0x243cf0,即resolve的值,会作为_0x1e72d1函数的列表类型的入参的第二个元素
继续跟

发现先去执行了_0x3edf5e函数,看一下_0x3edf5e的逻辑,主要是将传入的_0x48cfb7,也即resolve的值,包装成类似下面的形式
再往下看,可以发现一段流程后又回到了Promise0中的case 2,

可以看到这里面components的值和上面说的包装的形式很像,合理怀疑就是刚才的流程中一步步组装起来的,这里暂时不管
简单分析下case 2块,根据_0x4b4666是否有值,再结合components(这里的compoents对象可以先扣下来固定下来),组成新的对象_0x1938e6,后面经过hashComponents得到_0x297a60
_0x4d7c27就是当时的第二个入参,根据_0x298102["GenerateFP"](false, true)可知这里是true,也就是不需要切割,最后将处理好的_0x297a60交给Promise0的resolve
根据上面的分析,这里的Promise0处于resolved(进入下一个状态)后,即在_0x1e72d1函数中执行完了 _0x49bd55 = _0x41c1ce["call"](_0x160280, _0x4bd611),返回了[2],继续跟,走到default块

在第一个判断条件跳出,然后函数返回
此时done就标记为了true,继续跟,可以看到又执行了Promise0的外层Promise的then,即_0x4234aa所在的Promise,然后将resolve接收的参数交给then中的onFulfilled处理,即最终又回到了_0x1e72d1函数

可以看到,在这次then中,将Promise中的参数赋值给了最开始sent中要用到的_0x1df4cd,接下来调用函数,

在这次调用中又回到了最开始的getImage方法,跟进去

到了case 1块,通过sent获取对应的值,而这里就是刚刚流程中Promise0的case 2块中产生的参数,即_0x592344["hashComponents"]处理后的值,可以把这个函数整体扣下来处理
至此,围绕_0x1e72d1函数的Promise过程就分析完了,后续涉及Promise的取值流程与此类似。
搜一下变量名,定位到赋值的地方

这里要注意下_0x354394["GenerateFP"]和_0x298102["GenerateFP"]不是一个东西
_0x354394["GenerateFP"]是前面分析的canvas指纹那套流程
_0x298102["GenerateFP"]则是前面分析的Promise流程
knock就是前置分析中config接口返回的
所以_0x4eecbb就是额外加了knock参数来生成文本的canvas指纹生成逻辑得到的参数
定位到赋值的地方

也是通过sent拿到的,那按照上述分析的只要关注上个流程,也就是case 1块中的返回值就可以了
即[4, _0x298102["GenerateFP"](_0x2ac95b["knock"]["substr"](-5, 5))];
_0x298102["GenerateFP"]返回的依旧是之前分析过的那个Promise0,第一个参数是切割后的knock,第二个参数为undefined,在函数内会转换为false
那结合之前分析可知_0x1e45dc就是在components中额外加入了knock参数,再执行hashComponent,因为第二个参数是false,所以还要再切割一次得到最后的结果
先搜一下看一下赋值位置

发现运算中有个没看过的变量_0x323ff4,定位一下,发现就在上面,是写死的
那就直接把hex_md5扣下来算出_0xb0124就可以了
定位一下

发现其受_0x101aa8["ha"]影响,那先来看下_0x101aa8
同样的,后面涉及_0x101aa8的参与en运算的参数也是跟这次分析差不多的流程,后面不再赘述


可以看到其就是一个立即运行的函数,本质等于其返回值_0x57c9c0,也是个函数,而在_0x101aa8函数内部有很多给_0x57c9c0属性赋值的地方,其中就包括上述提到的ha,也可以看到ha的初始值为空
其实可以同时看到紧接着_0x101aa8的下面的变量也是个立即执行的函数

其接收的参数正是_0x101aa8,而它返回的也是它内部的函数_0x5918e4,简单看一下_0x5918e4的逻辑可以发现它会调用_0x101aa8
所以在_0x101aa8中不能只关注那些属性值的初始值,还要找到其他可能的二次赋值的地方
对于ha,可疑的只有下图中的位置

然后_0x582300也是通过sent得到,那看一下上一步返回的地方
正是正在分析的获取图片的流程,那也就是说此时ha还是初始值空,走完图片获取流程才会给ha再次赋值
那回到_0x2d7772这个参数的分析,根据判断逻辑,ha为空,那它就是一个固定值,至此该参数也分析完了
定位一下

_0x1ac30b等于_0x3d86c7
_0x3d86c7赋值的地方有两个,取决于case 2块中的返回值

如果hb不为空就会走到case 3,那_0x3d86c7就是_0x94b78b和sent的结果拼起来的,_0x94b78b就等于hb,sent对应的是上一步的返回值,即
也就是处理后的canvas指纹值
但根据上述_0x101aa8流程的分析可知hb此时是为空的,不会走到case 3,那_0x3d86c7也就是case 2中得到的固定值了,之后直接走到case 5,被赋给_0x1ac30b
定位一下

精简一下逻辑
先找到canStorageUse

canStorageUse在正常浏览器环境下返回的应该都是true,即_0x397b35为true
回到_0xc3f820的生成逻辑继续往下看,_0x2ac95b是getImage对应函数的入参,定位一下传入的位置

在loadFirstImg返回的函数中首次调用的getImage,入参为_0x1cc39a,主要用到的是this的属性,也就是_0x57c9c0的属性(又回到了_0x101aa8函数内的处理逻辑),这里主要看一下options赋值的地方

只有一个地方,也就是_0x57c9c0函数的入参就是对应后面的options,那调用栈往回看一下这个入参怎么生成的
下图中能看到这个入参是在验证码HTML页面渲染时绑定的时间监听中拿到的

那看一下该HTML的请求堆栈,其中有一个verify.xx.js(反不反混淆影响不大),搜索dfu能看到很像这个入参生成的地方

刷新一下发现最后定位在case 2中赋值的地方,其实对比一下逻辑,verify.xx.js中两处dfu赋值的地方的逻辑基本是一致的,至于走哪个分支里的赋值应该和缓存有一定的关系
用到的this['config']中的值也就是config接口返回的值,然后这里得到的newconfig就对应要找的入参
接着继续在verify.xx.js中看一下dfu生成逻辑中vaptchanu是在哪里设置的

可以看到是在setToken中将传入的参数切割后在localStorage中存成了vaptchanu,在这打断点没断住,那就是目前的流程不涉及verify.xx.js中的这个赋值流程,可以暂时不用管。
也就是说在verify.xx.js目前走过的流程中,dfu就是空,那再回到_0xc3f820的生成逻辑继续往下看,自然的_0x26f33d也就只和_0x101aa8["staticDfu"]有关
定位一下_0x101aa8["staticDfu"]赋值的地方,发现在图片获取流程和验证流程中都存在赋值的地方,同时这两处也都有对vaptchanu和vaptchaut的赋值
目前只用关注图片获取流程中的处理逻辑即可

打断点到赋值的地方,发现在请求完图片接口之后才会走到这来赋值,也就是staticDfu在图片接口的参数分析中可以理解为都是undefined,包括下面的staticDfuTrust目前也是undefined
即_0xe75d98和_0x26f33d这两个参数都没有值,那看一下compareDfu的逻辑

compareDfu简单分析下就是根据两个入参的长度来选择返回两个入参中的其中一个或返回一个固定值
综上所述,_0xc3f820就是compareDfu中返回的那个固定值
定位看一下逻辑
跟上面_0x26f33d这个值的分析差不多,在getImage流程中取的就是一个固定值
看下赋值逻辑
uaDelExtra对应的函数为

navigator["userAgent"],location["host"],_0x101aa8["secretC"]都相当于是定值,那拼出来的_0x260187也是定值,然后经过hex_md5处理再切割一下就得到了_0x37ab28
这里实际取的是globalMd5,看一下其对应的生成逻辑
可以定位到两个地方
先看第一个地方

这是在_0x101aa8内部的_0x57c9c0函数中,_0x4bcc6c是_0x57c9c0的入参,这个入参上面分析过了,这里直接看verify.xx.js
搜索md5能看到很像这个入参生成的地方

刷新一下发现最后定位在case 2中赋值的地方,对比一下逻辑,verify.xx.js中两处md5赋值的地方的逻辑也基本是一致的
用到的this['config']中的值也就是config接口返回的值,可以把md5生成中用到的hex_md5和splicingObj扣下来进行运算,这里的hex_md5和之前分析中出现的是一样的,那对应的globalMd5的值也就是这里md5的值
而定位到的另一处globalMd5赋值的地方是在getImage流程之后才会走到的,故getImage流程中不用管
结合算出来的en值拼成请求体后请求拿到图片,得到的图片是乱序的,还需要根据图片接口返回的数据来还原图片的顺序
根据图片接口返回的字段,搜一下img_order,发现有两个地方,其中一个地方存在Decrypt函数,可以在那里断一下

_0x39a52f就是接口返回的数据,主要看一下_0xef75b5怎么来的
其由_0x15d835、_0x11835f、parseInt(_0x57c9c0["secretC"])、_0x17f3de拼接而来,那接下来主要看一下这几个参数的赋值逻辑
定位一下,可以发现其初始值为0,后面还有一个赋值逻辑

简单分析下,就是如果接口中存在ha字段且ha不为空,就再次给_0x15d835赋值
可以把这里的hex2int函数扣下来,_0x354394["GenerateFP"]之前分析过,是涉及canvas指纹的地方,这里加了一个ha作为额外的文本内容来辅助生成canvas指纹
定位一下,其初始值也为0,后面有个二次赋值的地方

这里又出现了sent,那就看一下上一步的逻辑
判断接口中hb字段的情况,如果hb有值才会走到二次赋值的地方,二次赋值时主要关注这个返回值
还是之前分析过的_0x298102["GenerateFP"],在components拼接的逻辑中额外加了_0x39a52f["hb"]),后面又经过了hashComponents和切割
初始值为0,二次赋值的逻辑如下
所以要看一下globalPow是怎么生成的,这在下面_0x57c9c0中一起分析
这个其实也是在图片获取流程中分析过的,是_0x101aa8函数内部定义的一个函数,其中存在很多给自己属性赋值的操作,那就看一下globalPow和secretC是哪里赋值的
secretC之前定位过,是个定值,接下来主要看globalPow
定位到可疑的赋值位置

这里的_0x101aa8定位过去可以看到其实就是_0x57c9c0
globalPow是在onmessage里赋值的,那对应的看一下postMessage

其中涉及一个_0x58df75参数,也就是pow对应的函数的入参,定位一下看下哪里调用的pow

在_0x57c9c0["prototype"]["insertCaptchaImage"]函数的处理逻辑中调用的,而这里刚好也是初始化globalPow的地方,这里的_0x39a52f也是这个函数的入参,定位一下可以发现就是图片接口返回的数据
回到postMessage,跟进去发现真实的处理逻辑在work2.js中

这里可以把pow函数扣下来,e.data.str就是上述图片接口返回的数据中的r参数,走到self.postMessage后就能进入刚刚onmessage的地方,精简下逻辑,即globalPow等于pow(r)
综上得到_0xef75b5后就可以看下Decrypt函数
很简单的逻辑,可以直接用python复现一下
这样图片的真实顺序就还原完成了
按照上述图片接口的分析直接定位到关键参数en生成的地方

可以看到和图片接口的en值整体上的加密逻辑差不多
看一下_0x263810怎么来的
还是由一堆值拼接而来,继续一个个看
定位一下

可以看到是verify对应函数的参数,和getImage中的_0x2ac95b分析流程类似,先看一下哪里调用的verify

在_0x57c9c0["prototype"]["validateVaptcha"]里,同时verify的入参也是_0x57c9c0["prototype"]["validateVaptcha"]的入参
也可以看到入参_0x2a9d5a在_0x57c9c0["prototype"]["validateVaptcha"]中也增加了几个额外的参数
先定位一下哪里调用了validateVaptcha,可以搜一下这个关键字或者调用栈往回找

可以看到是在_0x57c9c0["prototype"]["canvasMouseUp"]里调用的,实际verify的部分入参也是在这里拼起来的,结合_0x57c9c0["prototype"]["validateVaptcha"]中增加的几个额外的参数,根据里面的逻辑来看一下几个关键的参数
dt
dt生成的逻辑如下
在上面就能看到_0x3b81b6的生成逻辑,是个时间差,带入上面的逻辑就能得到dt
ch
canvas画布的高度,定值(注意需要和实际的验证码区域区分一下)
cw
canvas画布的宽度,定值(注意需要和实际的验证码区域区分一下)
v
对应_0x3b0f05
_0x9a77c0就是处理后的轨迹坐标,_0x3dfad4["assemblyCoordData"]函数可以直接扣下来
p
对应_0x57c9c0["globalPow"],这个上面也分析过了,直接拿来用即可
dfu
对应this["options"]["dfu"],这个上面也分析了,在verify.xx.js中有相关生成逻辑,其中涉及到的vaptchanu也可以在相应的地方断一下,在这一步可以看到dfu最终得到的是空
[培训]Windows内核深度攻防:从Hook技术到Rootkit实战!
最后于 2025-9-24 09:48
被LotusRain编辑
,原因: 补充