本文章中所有内容仅供学习交流使用,不用于其他任何目的,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!若有侵权,请联系作者删除。
逆向目标
目标网站:
aHR0cHM6Ly93d3cuZGluZ3hpYW5nLWluYy5jb20vYnVzaW5lc3MvY2FwdGNoYQ==
关键要点:图像撕裂还原、轨迹、参数【lid, c, ac】
抓包分析
本篇文章以官网demo 5.1.53 版本进行分析,需要注意官网关键js文件每天会动态更新两次
进入网页,按F12打开调试窗口后刷新页面,建议每次都清理一下缓存或者直接使用无痕模式
刷新页面后定位到demo位置,点击滑动拼图查看发送请求

图片1.png (121.67 KB, 下载次数: 4)
下载附件
2025-8-13 16:25 上传
接口 a 请求载荷

图片2.png (72.3 KB, 下载次数: 4)
下载附件
2025-8-13 16:25 上传
这里只需要注意aid的生成
aid = 'dx-' + str(int(time.time() * 1000)) + '-' + str(random.randint(10000000, 99999999)) + '-3'
接口 a 响应内容

图片3.png (94.97 KB, 下载次数: 4)
下载附件
2025-8-13 16:25 上传
p1:背景图地址
p2:滑块图地址
sid 和 y 后面需要用到
当我们滑动滑块完成验证后会发送一个v1接口的请求

图片4.png (88.1 KB, 下载次数: 3)
下载附件
2025-8-13 16:25 上传
提示:本文截图中某些值可能和之前截图中的不一致,以文字说明为准,图知其意便可
ac:包含轨迹的加密参数
c:c1接口获取需要逆向
sid:a接口响应中获取
aid:这里aid要与a接口的aid一致
x:滑动距离
y:a接口响应中获取
v1接口响应中的token值是我们本次逆向的最终目标

图片5.png (39.22 KB, 下载次数: 3)
下载附件
2025-8-13 16:25 上传
流程梳理
1、第一次请求c1接口获取lid
2、第二次请求c1接口获取data值
3、请求a接口获取图片地址以及一些关键参数
4、请求v1接口完成验证获取token
图像还原
老样子打上canvas断点后点击验证滑块,断到如下图位置

图片6.png (66.08 KB, 下载次数: 4)
下载附件
2025-8-13 16:26 上传
分析入参
n:canvas对象
t:img对象
i:图片高度
e:图片高度
r:32位数组
分析代码
1、将一张图像完整绘制到canvas上
2、然后根据数组r的长度将图像水平分割成多个切片
3、再按照数组r的顺序重新排列这些切片绘制到canvas上
知道逻辑了那么就需要找到关键32位数组是如何生成的
继续往上跟栈找到生成位置

图片7.png (35.1 KB, 下载次数: 4)
下载附件
2025-8-13 16:26 上传
函数sn算法还原,传入a接口p1的值也就是背景图地址
def get_arr_32(self, img_url):
n = img_url.split("/")[-1].split(".")[0]
c = []
for i in range(len(n)):
u = ord(n)
while u % 32 in c:
u += 1
c.append(u % 32)
return c
图像还原算法
def draw_slices_from_array(self, image_content, output_path, arr):
src_img = Image.open(BytesIO(image_content))
src_width, src_height = src_img.size
canvas_width = 32 * 12
canvas_height = 200
dest_img = Image.new("RGB", (canvas_width, canvas_height), "white")
for index, x in enumerate(arr):
r = x * 12
if r + 12 > src_width:
continue
cropped = src_img.crop((r, 0, r + 12, 200))
dest_img.paste(cropped, (index * 12, 0))
dest_img.save(output_path)
还原结果

图片8.png (15.85 KB, 下载次数: 4)
下载附件
2025-8-13 16:26 上传

图片9.png (11.87 KB, 下载次数: 4)
下载附件
2025-8-13 16:26 上传
C 值分析
对v1接口的c值进行搜索后发现是在c1接口返回的
c1接口请求了两次
第一次是get请求,headers中有个短的加密参数Param
返回一个lid

图片10.png (46.11 KB, 下载次数: 4)
下载附件
2025-8-13 16:26 上传

图片11.png (40.61 KB, 下载次数: 4)
下载附件
2025-8-13 16:26 上传
第二次是post请求,表单中有个长的加密参数Param
返回一个data值也就是我们要的 c 值

图片12.png (109.6 KB, 下载次数: 4)
下载附件
2025-8-13 16:26 上传

图片13.png (27.59 KB, 下载次数: 4)
下载附件
2025-8-13 16:26 上传
第一次c1请求的 Param 分析
打上c1的xhr断点清缓存刷新页面
往上跟栈可定位到其生成位置在一个index.js文件中
注意,它有两个index.js文件,咱分析的这个是带?_t=***的

图片14.png (73.09 KB, 下载次数: 4)
下载附件
2025-8-13 16:26 上传
参数除lid外都是固定的
lid生成代码
l = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
lid = str(int(time.time() * 1000)) + ''.join(random.choice(l) for _ in range(32))
进入加密函数后可以看到是个控制流。

图片15.png (45.96 KB, 下载次数: 3)
下载附件
2025-8-13 16:26 上传
整个index.js文件也是存在大量的混淆,为方便读者阅读我这里用ast还原一下,代码就不公布了,写的比较渣,github上也有开源的。简单还原了一下大数组和一些拼接以及控制流这些,有兴趣的还是去找蔡老板进修一下
还原后看起来很清晰了,直接抠算法都很简单了

图片16.png (31.37 KB, 下载次数: 4)
下载附件
2025-8-13 16:26 上传
第二次c1请求的 Param 分析
和第一次一样的分析方式
或者直接断点在上面的加密位置上,两次的加密方法都是同一个
往上跟栈到如下位置

图片17.png (109.26 KB, 下载次数: 4)
下载附件
2025-8-13 16:26 上传
这次要加密的参数更多,所以加密后的Param长度也就更长了
ac 参数分析
给v1打上xhr断点,滑动滑块验证
跟栈跟栈
定位到一个Promise
代码往上看可以看到ac了,给打上断点再定位过去

图片18.png (63.9 KB, 下载次数: 4)
下载附件
2025-8-13 16:26 上传
可以看到在这里用getUA方法提取z里的ua的值,ua就是ac,早已生成了
聪明的我们往下看可以看到有个reload()方法,进去看看

图片19.png (62.19 KB, 下载次数: 4)
下载附件
2025-8-13 16:26 上传
进入reload方法后又是一个新的js文件greenseer.js
关键的算法都在这个文件中代码每天动态更新两次

图片20.png (27.65 KB, 下载次数: 2)
下载附件
2025-8-13 16:26 上传
又是大量的混淆,混淆方式和index.js一样,再解混淆方便各位阅读

图片21.png (69.63 KB, 下载次数: 4)
下载附件
2025-8-13 16:26 上传
清晰了,又可以愉快地分析了
分析reload方法可知这是在初始化z里的参数
最后再调用start方法
进start再看看

图片22.png (17.71 KB, 下载次数: 3)
下载附件
2025-8-13 16:26 上传
都是一些检测然后进行加密的流程
加密也都是一些简单的运算直接算法拿下就行
getTM:tm时间的加密
getBR:系统、浏览器版本的加密
getLO:document.referrer、location.href加密
getCF:随机数加密
getDI:判断window.top是否等于window.self以及window窗口宽高
getEM:自动化特征检测
getJSV:js版本号的加密
getTK:sid加密
getSC:window.screen加密
bindDomEvents:监听轨迹加密
所有方法最终都会调用app这个方法

图片23.png (10.6 KB, 下载次数: 4)
下载附件
2025-8-13 16:26 上传
进入app方法中可以看到最终ua的生成逻辑
将加密的参数追加到_ua中再将_ua经过btoa方法得到ua

图片24.png (20.25 KB, 下载次数: 4)
下载附件
2025-8-13 16:26 上传
进入bindDomEvents方法前已经生成了一个短的ua了
下面就是将轨迹加进去了

图片25.png (44.08 KB, 下载次数: 4)
下载附件
2025-8-13 16:26 上传
滑动距离识别随便找个开源ocr或者用cv2,注意图像的缩放
轨迹检测也不严格,随便网上找个或者模拟写个和浏览器上差不多的就行
轨迹算法
def get_track(self, distance):
def __ease_out_expo(sep):
if sep == 1:
return1
else:
return1 - math.pow(2, -10 * sep)
def random_randint(min, max):
range_val = max - min
rand = random.random()
num = min + round(rand * range_val)
return int(num)
slide_track = [[755, 325, 3526]]
count = 30 + distance // 2
t = random_randint(50, 100)
_x = 0
for i in range(1, count + 1):
sep = i / count
x = round(__ease_out_expo(sep) * distance)
t += random_randint(30, 50)
if x == _x:
continue
slide_track.append([755 + x, random_randint(320, 330), 3526 + t])
_x = x
return slide_track
轨迹加密共有三处
其中1和2不是强校验可以不处理不追加进ua
1、点击事件轨迹,将点击验证出现滑块图前的点击轨迹加密进去
2、移动事件轨迹,将点击验证出现滑块图前的移动轨迹加密进去
3、移动事件轨迹,将滑动滑块时的轨迹加密进去
这里只分析移动滑块轨迹的加密逻辑
进入recordSA方法
分析可知这里获取了pageX、pageY、轨迹点时间,加密后存入_sa数组

图片26.png (16.96 KB, 下载次数: 4)
下载附件
2025-8-13 16:26 上传
需要注意,滑动距离是有偏移计算的

图片27.png (98.48 KB, 下载次数: 4)
下载附件
2025-8-13 16:26 上传
recordSA方法之后需要进入sendSA方法处理_sa数组

图片28.png (40.26 KB, 下载次数: 3)
下载附件
2025-8-13 16:26 上传
sendSA方法就是遍历_sa数组调用app更新ua

图片29.png (9.58 KB, 下载次数: 4)
下载附件
2025-8-13 16:26 上传
sendSA完事后进入最后的sendTemp方法

图片30.png (41.31 KB, 下载次数: 4)
下载附件
2025-8-13 16:26 上传
传入一个xpath、x距离、y是获取图片地址的a接口响应中的参数
获取html标签中的一些信息再拼接上传入的参数进行加密更新ua

图片31.png (28.74 KB, 下载次数: 4)
下载附件
2025-8-13 16:26 上传
现在的ua就是我们需要的最终结果了
以上就是全部ac的逻辑了
结果验证

图片32.png (73.5 KB, 下载次数: 3)
下载附件
2025-8-13 16:27 上传
算法不难但建议找个第三方固定js的网站去练习
官网每天早上10点和晚上7点更新js(实际会延迟1个小时)
建议先用补环境方法理解全部过程后再抠算法