【验证码逆向专栏】V5验证码逆向分析
声明
本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!
本文章未经许可禁止转载,禁止任何修改后二次传播,擅自使用本文讲解的技术而导致的任何意外,作者均不负责,若有侵权,请在公众号【K哥爬虫】联系作者立即删除!
前言
最近有粉丝反馈关于 v5 验证的相关问题,不知道 wss 协议的滑块应该如何下手,该网站通过 wss 协议传输进行验证码的校验,本文就来针对这个demo站进行逆向研究。
逆向目标
- 目标:V5 验证Demo,滑块逆向分析
- 地址:aHR0cHM6Ly93d3cudmVyaWZ5NS5jb20vZGVtbw==
逆向过程
抓包分析
打开 demo 地址,发现有智能和滑块俩种形式。本文对滑块验证进行分析。
点击按钮进行验证,我们发现它进行WS协议传输,总共有 6 条数据交互,其中3条发送到服务器之后,我们得到1条响应,之后又向服务器发送了一次,再次得到了响应。
很明显发送和接收的消息都被加密了,拖动滑块进行校验,发现它又上传了一段密文,同时也对应接收到了消息,很显然这部分必然是校验的过程。
所以,接收和发送总共是8个ws数据交互,我们只需要将这8个分析完毕就可以解决。
逆向分析
三段上传包分析
看过往期 WS 协议文章的话,我们肯定知道 WS 首先找onopen ,我们从堆栈第一个进入。
我们发现它实例化了一个 WebSocket 对象,那它的入口必然就是这里,我们在这里下个断点,刷新页面,发现成功在这里断了下来。
我们的首要目的就是找到onopen,既然它实例化了,那么 onopen 大概率就是在附近,果不其然在下面我们找到了这个方法。
那么顺藤摸瓜,我们看看他内部消息加密是如何生成的,我们发现它内部只调用了 e 函数,那么我们进去。发现它又调用了 t 函数
再次进入 t 函数,发现有参数构造且传参过程,如下:
我们进入该函数中,看看是不是我们最终要找的消息加密的部分,进入后发现这个函数就是我们要找的消息加密主体,
通过 n = Cb(i)
获取加密明文,接着通过 e = f(i, o)
获取加密的 key ,发现 i=0 且是一串哈希值,通过得到的 key 对明文进行加密。接着进行进一步分析,i,o 来源自哪里,通过搜索可知,i 与 o 由页面html渲染,如下:
明文由 requestId,token 以及一些浏览器参数构成,大致如下:
{"requestId":"Req_17483265394231","command":"7051CA8BF3E64DEDAA9334620DA8F5F1","data":{"l":"9dbf055d86******","f":"eab39387d6a2acf6a1d71c379a5ba746b61fc759","m":"c225b29a*******","j":"ES5","tl":5,"o":{"spm":"c225b29****","v5lid":"OrOeKo62lbu*******","userAgent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) ******","language":"zh-CN","colorDepth":24,"deviceMemory":8,"pixelRatio":1.25,"hardwareConcurrency":8,"screenResolution":[1536,864],"availableScreenResolution":[824,1536],"timezoneOffset":-480,"timezone":"Asia/Shanghai","platform":"Win32","canvas":"e1547d5eeb5c65***********","webgl":"164641c74782e718**********","touchSupport":[0,false,false],"audio":"124.04347527516074","j":"ES5"},"exfp":{"fpa":"902f0fe98719b779ea*******","fphc":"e1547d5eeb5c6506*******","fphg":"164641c74*****","fphf":"eab39387d6a2acf6a1d*******","fprt":"77c2955771*************"},"aux":{}}
l 值为页面token值,requestId由时间戳构成,其余参数可以自己进行伪造,最终通过AES进行加密,然后拼接分成3段进行WS 协议传输。
e= encryptData(n, e)
a = [];
a["push"]("7051CA8BF3E64DEDAA9334620DA8F5F1")
a["push"](token)
a["push"](e)
var c = a.join(""); // Join the array into a string
// console.log(c);
var C = 0;
var res = [];
while (c) {
var e = null;
// If the string is longer than 1024 characters, slice it
if (c.length > 1024) {
e = c.substr(0, 1024);
c = c.substring(1024);
} else {
e = c;
c = null;
}
// Format and push the chunk into the result array
e = "0" + "|" + "3" + "|" + C + "|" + e;
C++;
res.push(e);
}
至此3段ws发送就分析完毕,经过我们最开始分析可知,发送之后会得到一段加密的密文返回,我们尝试 python 封装果真得到一段加密的密文返回。了解WS的知道,返回值的处理一般都在 onmessage 中,所以我们找 onmessage 事件处理器。
单步跟进,就进入到了解密的关键地方,如下图:
a[ca](ob, function(e) {
var t = e[oa], n = a[wa], r = a[xa];
e = t;
r || (r = t.substring(0, 32), e = t.substring(32), a[xa] = r);
t = f(r, n);
e = h(e, t);
e = Bb(e);
t = e[fb] || qb;
t == qb ? (t = e[lb], n = a[S][t]) && (a[S][t] = null, delete a[S][t], (t = n[Gc]) && t(e)) : a[N](t, [ e ]);
});
解密的密钥是由返回的密文截取32位与token 通过熟悉的 f 方法来生成,接着进行常规的AES解密即可得到,python将操作进行复现:
解密后它给我们返回了一个 u 值,我们暂且不知道这个u有什么作用,大概率需要参与后面的计算。
获取图片
获取图片的上传接口,加密位置与三段ws传输位置相同,不同的是,加密key 的生成由返回的 u 值与token通过 f 方法生成。
紧接着还是通过拼接token进行生成密文,需要注意 e 的数字索引不能写错,否则会报错。
e= encryptData(n, e)
a = [];
a["push"]("E97CE473AE1A46A8BF4A88FD73636D7E")
//a["push"](token)
a["push"](e)
var c = a.join("");
// console.log(c);
var C = 0;
var res = [];
while (c) {
var e = null;
if (c.length > 1024) {
e = c.substr(0, 1024);
c = c.substring(1024);
} else {
e = c;
c = null;
}
e = "1" + "|" + "1" + "|" + C + "|" + e;
C++;
res.push(e);
}
最终得到图片密文,再次通过相同的解密方法进行密文的解密,不同的就是解密的 key 仍然是使用 u 值与token进行生成,最终成功得到图片数据。
接口验证
拖动进行验证,同样还是在相同地方断住,轨迹明文如下:
"1748328830478,90,15,-17,90,15,-17,105,16,-16,112,22,-15,120,32,-14,126,44,-14,134,55,-13,141,66,-13,149,74,-13,156,82,-13,164,86,-13,172,89,-13,179,91,-13,188,91,-13,205,91,-13,247,92,-13,247,94,-13,262,95,-13,262,97,-13,269,100,-13,277,103,-13,291,106,-13,299,108,-13,306,111,-13,314,113,-13,322,115,-13,329,118,-13,336,119,-13,344,123,-13,351,125,-13,359,127,-13,366,129,-13,374,131,-13,381,132,-13,404,134,-13,405,135,-13,426,137,-13,427,139,-13,434,142,-13,442,147,-14,449,151,-14,457,156,-15,472,162,-15,472,164,-15,547,167,-15,1748328831257"
初步分析,轨迹由时间戳,时间增量,x偏移,y偏移组成,经过可知时间增量不能与实际时间增量变化太大,轨迹生成如下:
import random
import time
def generate_slider_track(x_distance):
# 生成开始和结束时间戳(当前时间)
start_time = str(int(time.time() * 1000))
end_time = str(int(time.time() * 1000) + 1000)
track = [start_time]
current_x = 0
remaining_distance = x_distance
y_base = -24 # 基础y坐标
time_increment = 0
while remaining_distance > 0:
step = min(max(1, int(remaining_distance * random.uniform(0.1, 0.3))), remaining_distance)
current_x += step
remaining_distance -= step
# 时间增量(随机但逐渐增加)
time_increment += random.randint(30, 100)
y_offset = random.randint(-3, 0)
y = y_base + y_offset
track.extend([str(time_increment), str(current_x), str(y)])
if int(track[-2]) != x_distance:
time_increment += random.randint(10, 50)
track.extend([str(time_increment), str(x_distance), str(y_base)])
track.append(end_time)
track_str = ",".join(track)
return track_str
# x_distance = 72 # 你想要移动的距离
# track_str = generate_slider_track(x_distance)
# print(track_str)