滑块逆向-FK滑块逆向

请求流程分析

https://i.fkw.com/?_ta=207

首先是一个get请求,参数里有bsskeybss,返回一个base64编码的字符串imgId

随后会把这个imgId作为参数去请求图片data:image/jpeg;base64,iVBORw0KGgoAAAAN...

滑动滑块的过程数据包如下,带有bsskey参数和vi参数

vi就是轨迹验证参数,一般是xydate,等等组合的数据,后端会校验vi是否是正常轨迹,如果通过则会返回checksign

后续会带上checksign去登录

图片请求

请求的base64解码之后的图片直接就是完整图片,不需要还原

现在就是去分析bsskey参数,通过函数栈在e.registerWithLogger下断点

断下来之后直接看函数栈就行,或者跟一下也行,可以发现在_initCaptchaComp函数中,bsskey是一个被固定的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import requests
from base64 import b64decode

url='https://cv.fkw.com/verify/get'
data={"bss":4,
"bssKey":"ALihhLoGCAEQBBoHWzEsMyw1XSIJ6ZKf55Sz5qC5",
"appKey":1,
"version":"1.0.1"}
headers={
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36 Edg/111.0.1661.54',
'Accept': 'application/json'
}
html=requests.post(url,headers=headers,json=data).json()
if html['success']:
bs64img=html['msg']['imgId']
else:
print('请求图片失败,原因如下:',html['msg'])

with open('fkimg.png','wb')as f:
f.write(b64decode(bs64img))
print('FK图片保存成功')

轨迹vi参数生成

validate请求上的函数栈打下断点,断在t.exports函数处

断下来之后再去看函数栈,跟到e.validateWithLogger的时候发现参数还是加密的,说明加密的函数还在前面

往上找到匿名函数,可以发现这里参数是没加密的,可能加密逻辑就在这里,在匿名函数处下断点

1
2
3
4
5
6
7
8
9
w.on("validate", (function(t) {
var n = (0,
u.encrypt)((0,
o.default)({}, t, {
eo: x, //eo的值是前面get请求图片的时候返回
bss: v, //前面get请求图片的时候返回
bssKey: g,//固定值
validateSign: b//前面get请求图片的时候返回
}));

通过查看变量可以发现,此时t变量是明文,n是加密之后的参数,所以加密逻辑在这里

先解释一下t里面的各个参数的含义

downY是鼠标按下的时候,鼠标距离浏览器上端的距离,upY是鼠标抬起的时候,鼠标距离浏览器下端的距离,这两个参数一般不用在意,固定一个值就行

left是滑块滑动了多少距离,这个和滑块缺口有关

track值如下

track怎么生成的呢?既然这里有w.on("validate", (function(t)监听事件,那么肯定在前面有触发这个信号的

继续往上追函数栈可以发现,e.emit函数触发了validate信号

先看段代码,包含了几个参数

1
2
3
4
5
6
e.emit("validate", {
left: m(),
downY: f,
upY: i,
track: c
}),

这里的left参数的值是m()函数返回的,m()函数就是返回滑块滑动的距离,这里不再赘述

c的生成逻辑如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
n._bindEvent = function(t) {
var e = this
, n = []
, r = (0,
l.throttle)((function(t) {
n.length < 20 ? n.push([t.clientX, t.clientY, (new Date).getTime(), 0, 0]) : l.EventHandler.off(document, "mousemove", r)
}
), 500);
...
return function(t) {
c.length < 40 && c.push([t.clientX, t.clientY, (new Date).getTime(), 1, 1]),
...
}(t),
[function(t) {
c.length < 40 && c.push([t.clientX, t.clientY, (new Date).getTime(), 2, 1]),
...
}
, function(t) {
c.length < 40 && c.push([t.clientX, t.clientY, (new Date).getTime(), 3, 1]);
var i = t.clientY;
e.emit("validate", {
left: m(),
downY: f,
upY: i,
track: c
}),

这里clientX是距离浏览器左侧的距离,clientY是距离浏览器上侧的距离,clientY一般可以只变一两个像素

0,0的序列实际上不用管,可以写成固定的,至于原因,我也没分析出来,从1,1开始到最后的2,1结束,才是真正滑块移动的轨迹,而且最后一个2,1序列的clientX减去第一个1,1序列的clientX的值,就是参数left的值

最后分析一下加密函数,也就是这里的o.defaultu.encrypt

1
2
3
4
5
6
7
8
9
w.on("validate", (function(t) {
var n = (0,
u.encrypt)((0,
o.default)({}, t, {
eo: x, //eo的值是前面get请求图片的时候返回
bss: v, //前面get请求图片的时候返回
bssKey: g,//固定值
validateSign: b//前面get请求图片的时候返回
}));

单步跟一下会发现o.default其实啥也没做,主要是u.encrypt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
e.encrypt = function(t) {
var e = t.left
, n = t.downY
, r = t.upY
, a = t.track
, s = t.eo
, c = t.validateSign
, u = t.bss
, l = t.bssKey;
return {
appKey: 1,
et: s.et,
bss: u,
bssKey: l,
version: "1.0.1",
vi: (0,
o.btoa)((0,
i.default)({
s: 10,
l: e,
dy: n,
uy: r,
t: a,
validateSign: c
}))
}
}

这里i.default其实就是return r.JSON.stringify.apply(null, arguments) ,也就是把参数给转字符串了,加密函数在o.btoa

1
2
3
4
5
6
7
8
9
e.btoa = function(t) {
for (var e, n, r = String(t), i = 0, s = o, c = ""; r.charAt(0 | i) || (s = "=",
i % 1); c += s.charAt(63 & e >> 8 - i % 1 * 8)) {
if ((n = r.charCodeAt(i += 3 / 4)) > 255)
throw new a("'btoa' failed: The string to be encoded contains characters outside of the Latin1 range.");
e = e << 8 | n
}
return c
}

先扣这么一段代码

1
2
3
4
5
6
7
8
9
10
11
function encParam(t) {
for (var e, n, r = String(t), i = 0, s = o, c = ""; r.charAt(0 | i) || (s = "=",
i % 1); c += s.charAt(63 & e >> 8 - i % 1 * 8)) {
if ((n = r.charCodeAt(i += 3 / 4)) > 255)
throw new a("'btoa' failed: The string to be encoded contains characters outside of the Latin1 range.");
e = e << 8 | n
}
return c
}

console.log(encParam('{"s":10,"l":214,"dy":402,"uy":380,"t":[[577,522,1742127604322,0,0],[664,407,1742127604822,0,0],[626,402,1742127605085,1,1],[625,402,1742127605088,2,1],[626,402,1742127605182,2,1],[627,402,1742127605186,2,1],[628,402,1742127605188,2,1],[629,402,1742127605190,2,1],[631,402,1742127605192,2,1],[632,402,1742127605194,2,1],[632,402,1742127605196,2,1],[635,402,1742127605197,2,1],[636,402,1742127605200,2,1],[636,402,1742127605202,2,1],[638,402,1742127605204,2,1],[640,402,1742127605207,2,1],[642,402,1742127605208,2,1],[644,402,1742127605210,2,1],[644,402,1742127605212,2,1],[648,402,1742127605214,2,1],[648,402,1742127605216,2,1],[649,401,1742127605217,2,1],[651,401,1742127605219,2,1],[653,401,1742127605222,2,1],[654,401,1742127605224,2,1],[655,401,1742127605226,2,1],[656,401,1742127605228,2,1],[658,401,1742127605230,2,1],[659,401,1742127605231,2,1],[660,401,1742127605234,2,1],[660,401,1742127605235,2,1],[661,401,1742127605238,2,1],[664,400,1742127605240,2,1],[665,400,1742127605244,2,1],[668,400,1742127605248,2,1],[668,400,1742127605249,2,1],[669,399,1742127605254,2,1],[670,399,1742127605256,2,1],[671,399,1742127605258,2,1],[672,399,1742127605262,2,1]],"validateSign":"AgpkMlZDYW5NV3Z0Cg4xMTUuMTcxLjIxLjEyNBABGgYxNTQzMjI"}'))

参数是在打断点的时候直接得到的

运行的时候报错ReferenceError: o is not definedo是固定值ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=

剩下就是分析这里的几个参数是怎么来的了

1
2
3
4
5
6
7
8
vi: (0,o.btoa)((0,i.default)({
s: 10,
l: e,
dy: n,
uy: r,
t: a,
validateSign: c
}))

s是固定值,l是向右滑动的距离,dy是鼠标按下的时候距离上端的距离,uy是鼠标松开的时候距离上端的距离,t的话就是伪造就行了

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import requests
from base64 import b64decode
import py_mini_racer
import json

url='https://cv.fkw.com/verify/get'
data={"bss":4,
"bssKey":"ALihhLoGCAEQBBoHWzEsMyw1XSIJ6ZKf55Sz5qC5",
"appKey":1,
"version":"1.0.1"}
headers={
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36 Edg/111.0.1661.54',
'Accept': 'application/json'
}
html=requests.post(url,headers=headers,json=data).json()
if html['success']:
#print(html)
bs64img=html['msg']['imgId']
validateSign=html['msg']['validateSign']
print('validateSign:',validateSign)
else:
print('请求图片失败,原因如下:',html['msg'])

with open('fkimg.png','wb')as f:
f.write(b64decode(bs64img))
print('FK图片保存成功')

def get_vi():
with open('enc.js','r',encoding='utf8') as f:
ctx = py_mini_racer.MiniRacer()
ctx.eval(f.read())
args = {"s":10,
"l":214,
"dy":402,
"uy":380,
"t":[[577,522,1742127604322,0,0],[664,407,1742127604822,0,0],[626,402,1742127605085,1,1],[625,402,1742127605088,2,1],[626,402,1742127605182,2,1],[627,402,1742127605186,2,1],[628,402,1742127605188,2,1],[629,402,1742127605190,2,1],[631,402,1742127605192,2,1],[632,402,1742127605194,2,1],[632,402,1742127605196,2,1],[635,402,1742127605197,2,1],[636,402,1742127605200,2,1],[636,402,1742127605202,2,1],[638,402,1742127605204,2,1],[640,402,1742127605207,2,1],[642,402,1742127605208,2,1],[644,402,1742127605210,2,1],[644,402,1742127605212,2,1],[648,402,1742127605214,2,1],[648,402,1742127605216,2,1],[649,401,1742127605217,2,1],[651,401,1742127605219,2,1],[653,401,1742127605222,2,1],[654,401,1742127605224,2,1],[655,401,1742127605226,2,1],[656,401,1742127605228,2,1],[658,401,1742127605230,2,1],[659,401,1742127605231,2,1],[660,401,1742127605234,2,1],[660,401,1742127605235,2,1],[661,401,1742127605238,2,1],[664,400,1742127605240,2,1],[665,400,1742127605244,2,1],[668,400,1742127605248,2,1],[668,400,1742127605249,2,1],[669,399,1742127605254,2,1],[670,399,1742127605256,2,1],[671,399,1742127605258,2,1],[672,399,1742127605262,2,1]],
"validateSign":validateSign}
result = ctx.call("encParam",json.dumps(args))
print("vi的值:",result)
url = 'https://cv.fkw.com/verify/validate'
data = {
"appkey":1,
"et":6,
"bss":4,
"bssKey":"ALihhLoGCAEQBBoHWzEsMyw1XSIJ6ZKf55Sz5qC5",
"version":"1.0.1",
"vi":get_vi()
}
html = requests.get(url,headers=headers,data=data)

轨迹伪造

0,0的序列可以不用管,固定就行,从1,1开始的x坐标,到2,1或者3,1结束的x坐标,两者之差要等于滑动的距离,y的坐标可以不变,x轴的递增可以随机递增,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
import random
import time

start_time=int(time.time() * 1000)

def get_time():
global start_time
start_time+=random.choice([2,4,6])
return start_time

def get_random():
return random.choice([-1,0,1])

def get_trakcs(pos):
startpos=714
endpos=pos+startpos
print(f'startpos:{startpos} endpos:{endpos}')

tracks=[
[
894,
33,
get_time(),
0,
0
],
[
731,
547,
get_time(),
0,
0
],
[
723,
535,
get_time(),
0,
0
],
[
830,
535,
get_time(),
0,
0
]
]
# 因为前面固定了4个0,0序列的轨迹,所以这里滑动的距离如果大于36,就不能直接生成轨迹,不然轨迹序列个数就超过40个了
# 也就是要控制序列个数不超过40
if pos < 36:
random_numbers = [random.randint(startpos, endpos) for _ in range(pos)]
else:
random_numbers = [random.randint(startpos, endpos) for _ in range(35)]

sorted_random_numbers = sorted(random_numbers)
epoch = 1

for index,number in enumerate(sorted_random_numbers):
tracks.append(
[
number,
535+get_random(),
get_time(),
epoch,
1
]
)

if (index+1)%12==0:
epoch+=1

tracks.append(
[
endpos,
535+get_random(),
get_time(),
3,
1
]
)
print("tracks:",tracks)
print("tracks length:",len(tracks))
return tracks

if __name__ == '__main__':
get_trakcs(35)