声明
本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!
本文章未经许可禁止转载,禁止任何修改后二次传播,擅自使用本文讲解的技术而导致的任何意外,作者均不负责,若有侵权,请在公众号【K哥爬虫】联系作者立即删除!
前言
之前私信、星球提问中,有不少小伙伴咨询了某手滑块验证码的相关问题,该验证码整体难度不算高,不过有些细节会导致最终验证的成功率。该站过验证码其实就是激活 cookie 中 did 的过程,流程中存在问题,还可能出现过了验证码,但是 did 仍不可用的情况,ip 够纯的话,不会触发验证码风控(个人主页)。非业务项目,本文仅对个人视频主页的滑块验证码进行逆向分析,别处的风控未做研究,仅供学习交流:
逆向目标
- 目标:某手 web 端滑块验证码
- 网址:
aHR0cHM6Ly93d3cua3VhaXNob3UuY29tL3Byb2ZpbGUvM3hnbWFhdjI5NjhiaDdj
抓包分析
清除缓存,刷新网页,就能触发滑块验证码风控(没触发就反复操作几次):
前言中提到,过验证码是激活 cookie 中 did 的过程,did 是个人主页接口响应返回的(不要写成随机数):
/captcha/sliding/config
接口返回背景图、滑块图链接等,其中 disY 会参与到关键加密参数的生成:
disY 以及滑动距离需要进行缩放处理,原图大小为 686x400,网页为 316x312,缩放比例为 0.46 即可:
/config
接口有个请求参数 captchaSession,是下图接口返回的,不同的 /graphql
接口,请求参数 operationName 不一样,这也是个关键接口,did、session 等会导致响应结果不一样,不过都能提取到 captchaSession 参数:
{'data': {'result': 400002, 'jsSdkUrl'
:环境、轨迹校验较严,后续环境不对,会触发二次验证(350029,文字点选、旋转验证),或一次验证通过,但无法获取到主页数据;{'errors': [{'message': 'Need captcha'
:环境、轨迹校验较松,一次验证通过,能获取到主页数据。
/captcha/sliding/kSecretApiVerify
为校验接口,请求参数 verifyParam 加密了环境、滑动距离以及轨迹等参数,风控点,响应如下则代表验证通过:
某手的滑块验证码抽象就抽象在,调试的时候会发现,手动滑过了验证码,也渲染不出主页,没过有时候反而出来了,当然,这就不深究了,脚本的实现效果是正常的。
经过测试,kSecretApiVerify 接口还包括但不限于如下的一些响应情况:
- 轨迹、verifyParam 为空:{“result”:350001,“desc”:“params err. check it”,“unifiedType”:2};
- 缺口识别错误:{“result”:350002,“desc”:“verify err.try again”,“unifiedType”:2};
- verifyParam 中的 captchaSn 错误:{‘result’:350005,‘desc’:‘captchaSN err’,‘unifiedType’:2};
- 验证时间过长,captchaSn 失效:{“result”:350009,“desc”:“captchaSN expired, try to get a new captchaSN to verify”,“unifiedType”:2};
- 轨迹、环境错误:{“result”:350014,“desc”:“anti check err, try to get a new captchaSN to verify”,“unifiedType”:2};
- 参数失效:{“result”:350017,“desc”:“verify err. try again”,“unifiedType”:2};
- 触发文字点选:{‘result’: 350029, ‘captchaSession’: ‘xxx’, ‘type’: ‘4’, ‘configPath’: ‘/rest/zt/captcha/wordClick/config’};
- 触发旋转验证:{‘result’: 350029, ‘captchaSession’: ‘xxx’, ‘type’: ‘2’, ‘configPath’: ‘/rest/zt/captcha/rotating/config’}。
逆向分析
建议整个指纹浏览器调试,便于对比分析。
从验证接口 kSecretApiVerify
的堆栈中跟进到 index.28acbed8.js
文件处,该文件并非固定链接,域名是动态的,可以考虑固定一套。直接 ctrl f 局部搜索加密参数 verifyParam,可以定位到下图处,下断后滑动缺口即可断住:
由上图可知,这块是个 switch-case 条件控制语句,流程执行到 case 10 时,verifyParam 也就是 a 参数,已经生成了。我们向上回溯,发现,整体的执行流程,是按照 case 0、3、10、end 这个顺序的,verifyParam 从 0 到 3 就生成了,证明加密部分在 Object(Ht["a"])(c)
中异步实现的。
跟栈分析前,我们先来看看 c 是什么,下面逐一分析下:
- bgDisHeight:定值,背景图片原图高 * 0.46;
- bgDisWidth:定值,背景图片原图宽 * 0.46;
- captchaExtraParam:关键参数,浏览器环境、指纹,ua、canvas、时区以及电池电量等信息;
- captchaSn:
/graphql
接口响应返回; - cutDisHeight:定值,滑块图片原图高 * 0.46;
- cutDisWidth:定值,滑块图片原图宽 * 0.46;
- gpuInfo:浏览器环境,WebGL、显卡等信息;
- relativeX:滑动距离;
- relativeY:disY * 0.46;
- trajectory:转换处理后的轨迹。
再具体的跟下 c,先 ctrl f 局部搜索 trajectory,即可定位到上述参数生成的位置,轨迹 trajectory 也就是 a 参数切掉 ,
后得到的:
a 的具体转换位置就在上面 case 0 处,c 就是轨迹的起始时间戳,可以直接用 python 复现,轨迹算法自写、AI 调优或者寻找资源都行,轨迹算法不正确,会导致以下情况出现(后文环境参数指 canvasGraphFingerPrint 和 canvasGraph,环境黑的时候,需要注意 ip 是否也一起黑了):
- 虽然 y 轴校验的并不严格,不过若写成固定值,也可能会导致二次验证或 350014,ip 被风控;
- t 轴算法不正确,会导致环境参数直接黑掉一段时间,从而一直需要二次验证(350029);起始点不对,会导致 350014。
n = {
"trajectory": track
}
c = n["trajectory"][0][2] # 基准时间
trajectory = ",".join(
f"{point[0]}|{point[1]}|{point[2] - c}"
for point in n["trajectory"]
)
接下来主要的就是 captchaExtraParam 参数,和 trajectory 同在 case 4 中,其由 JSON["stringify"](i)
生成,i 中包含一堆环境参数:
直接搜索 key35,定位到后会发现,key1 到 key39 都是从 n 对象中取到的值,向上跟,captchaExtraParam 中的参数,全在这一块生成的,是一些环境、指纹参数,包括 userAgent、language、canvas、did 以及鼠标事件等等。有些参数不同浏览器环境,是一致的,不过有的不能写成固定值,比如 key2 复用,同样会导致环境参数直接黑掉一段时间,一直触发二次验证(350029),ip 也有关系,剩下的可以自行研究下,替换测试就知道校验哪些了:
最后,重新回到 Object(Ht["a"])(c)
处,分析下 verifyParam 参数是如何加密生成的。从此处,单步向上跟,一会儿就能跟到下图所示的,加密参数生成的位置,也可以直接搜索 regeneratorRuntime[r
快速定位:
这里对 c 对象进行序列化、转 Uint8Array、s[r("0x2d")](x, o)
异步处理后生成了 n 数组,最后经过 base64 编码得到的 verifyParam 参数值,a.a[r("0x34")](c)
处写法可参考:
'&'.join(f'{quote(str(k), safe="")}={quote(str(v), safe="")}' for k, v in c.items())
从 case 0 处单步向上跟,进到 s[r("0x2d")](x, o)
中,会跟到下图所示位置,这里创建了一个 Promise 对象,l 为定值,控制台打印这部分,可以看到,PromiseResult 就是 n 数组:
Jose.call("$encrypt", [...])
就是关键方法,选中 Jose[r("0x21")]
跟到 encrypt.ee7d2a41.js
文件中去,搜索 Jose,可以定位到 exports.Jose = o()
处,代码拿到本地,直接 window.Jose = o()
导出调用,缺补多删即可:
async function encrypt(e) {
var l = "c7b645db-65e8-401f-b38c-4c07c5fff247";
return new Promise((function (t, c) {
window.Jose.call("$encrypt", [e, l, {
suc: t,
err: c
}])
}
))
}
function h(e) {
for (var t = e["length"], c = new Uint8Array(t), a = 0; a < t; a++)
c[a] = e["charCodeAt"](a);
return c
}
async function asyncVerifyParam(dataArray) {
try {
let encryptedData = await encrypt(h(dataArray));
let verifyParam = new Buffer.from(encryptedData).toString('base64');
console.log(verifyParam);
} catch (error) {
console.error(error);
}
}
二次验证
有很多小伙伴都会疑问,为何自己的验证结果一直是 350029,这就是触发二次验证了,二次验证有两种类型的验证码,分别是文字点选(type 4)和旋转(type 2),根据前文的内容,想必大伙也知道了部分会导致二次验证的原因,文末至此总结一下,当然,可能会有遗漏,欢迎评论区补充交流:
- captchaExtraParam 中的 key2 复用,时间戳未动态生成;
- 环境参数 canvasGraphFingerPrint 和 canvasGraph 黑了,导致黑的原因前文有讲;
- ip 黑了。