LINUX SB 快照站

网易易盾滑块纯算逆向记录精华

原帖: linux.sb/topic/12971 · 共 15 楼 · 标题快照 2026-08-16 11:44:52

楼主 · 历史 (1)
发帖 2026-08-16 11:42:05 · 快照 2026-08-17 20:25:17

目标:看雪登录(passport.kanxue.com)的网易易盾滑块,纯 Node 全自动通过。
结果:/api/v3/check 返回 result:true,连续 5/5;最终 validate 提交看雪登录接口被接受。

版本信息(所有结论绑定此版本):

验证码 IDe019d55cf18348519c201a0d2f80ebfb
加载器load.min.js v2.5.4
corecore-optimi.m25b40.v2.28.5.min.js(631KB)
协议c.dun.163.com/api/v3/*

一、抓包:三个接口

登录页点出滑块,Network 里就三个接口:

接口作用关键参数
GET /api/v2/getconf初始化配置返回 dtresourcesapiServerstaticServers
GET /api/v3/get拉验证码fpcbdtid…返回 token + 背景图/拼图 URL
GET /api/v3/check提交结果tokendatacb…返回 result + validate

image-20260816104240379

PixPin_2026-08-16_10-49-11

get 请求参数(浏览器原样抓的)

PixPin_2026-08-16_10-41-37

referer=https://passport.kanxue.com/user-account_login.htm
zoneId=CN31          # getconf 下发
dt=...               # getconf 下发
acToken=             # 本 captchaId 为空
id=e019d55cf18348519c201a0d2f80ebfb
fp=...               # 指纹,见 3.1
https=true
type=                # 空 = 默认滑块
version=2.28.5
dpr=1
dev=3                # 设备类型
cb=...               # 随机串加密,见 3.2
ipv6=false
runEnv=10
group= scene=
lang=zh-CN
sdkVersion=
loadVersion=2.5.4
iv=4                 # IV_VERSION,core 常量
user=
width=320
audio=false
sizeType=10
smsVersion=v3
token=               # 刷新时带上旧 token
callback=__JSONP_xxx_0

check 请求参数

PixPin_2026-08-16_11-27-18

id, token, dt, zoneId, referer
data={"d":"...","m":"","p":"...","f":"...","ext":"..."}   # 核心
width=320  type=2  version=2.28.5  cb=...  bf=0  runEnv=10  iv=4  loadVersion=2.5.4

data 四个密文字段(m 恒为空),全部由本地生成。


二、解混淆:两层字符串表

core 六百多 KB,字符串全部收进一张表,调用统一走 a0_0x1e60(0x..)

var a0_0x3d6e=['doIJY3r0CJ','bDgVsRFVwz8', ...共1545条];
function a0_0x1e60(_0x3d6ee5,_0x1e6084){
  // 自定义 base64 字母表: kyclmxfigrhbswquanzevpjotdKYCLMXFIGRHBSWQUANZEVPJOTD0123456789+/=
  // 解码: 自定义base64 -> %xx -> decodeURIComponent
}

注意:不是标准 base64,字母表是自定义的。解码器自带 gtHdNm 方法,内部做了缓存。

更坑的是:解完这层,最底下的大模块还有第二层——自己的数组、自己的解码器,字母表是 2izvR3Ydkgw605lf(16 字符,映射到 hex)。

PixPin_2026-08-16_11-28-37

解混淆不硬啃,三步:

  1. 把数组 + 解码器原样抠出来,丢 Node vm 里跑,让解码器吐出全表
  2. 正则把 a0_0x1e60(0x..) 全部替换成明文
  3. 用 Babel 解析整个包,定位 webpack 模块数组,逐个拆出,共 77 个模块

三、参数逐个还原

3.1 fp

getfp 是浏览器指纹。全局搜 gdxidpyhxde 只找到一处读取,没有写入——因为生成它的模块把字符串名也编进了自己的嵌套表。

实际链路:

  • cookie 里存 gdxidpyhxdE
  • window.gdxidpyhxde 由 cookie 解码而来
  • 格式:自定义base64密文:时间戳,如 VppCnhP7SV+Y\pbQCE8...:1786798405363

PixPin_2026-08-16_11-29-34

处理:把生成模块原样塞进 Node vm,补假的 window/document/navigator/localStorage/screen,跑完 window.gdxidpyhxde 就有值了,与浏览器一致。整个指纹生成器不用自己复刻。

PixPin_2026-08-16_11-29-46

3.2 cb

cb 是每次请求都会变的随机串:

uuid = 32 位随机 [0-9A-Za-z]
在位置 [1, 10, 12, 13, 26, 31] 依次注入 "vfnv46" 六个字符
cb = aes(uuid)

suffixcodepos 三个常量(m25b40 / vfnv46 / 位置数组)与版本绑定,换版本要重取。

3.3 自研 aes

这个 aes 不是 AES,是易盾自己搓的分组密码。常量:

常量
SEED_KEYfd6a43ae25f74398b61c03c83be37449
ROUND_KEY037606da0296055c
SBOX256 字节置换表(__SBOX__
私有 base64 字母表MB.CfHUzEeJpsuGkgNwhqiSaI4Fd9L6jYKZAxn1/Vml0c5rbXRP+8tD3QTO2vWyo
填充符7

加密流程:

  1. 明文 UTF-8 字节 + CRC32(8 字节 hex)
  2. 按 64 字节分块,末尾补零 + 4 字节长度
  3. 密钥:SEED_KEY 扩到 64 字节,异或 4 字节随机数,再扩回 64 字节
  4. 每块做轮函数:ROUND_KEY 每 2 字节一组(操作码 + 操作数),对块做异或/加法,共 4 轮
  5. 链式:块^密钥 → 加prev → 异或prev → S盒(S盒()),输出即下一轮 prev
  6. 结果前 4 字节放随机数,最后私有 base64 编码

轮函数操作表(ROUND_KEY = 03 76 06 da 02 96 05 5c):

操作码操作
0不变
1全块异或常量
2全块加常量
3逐字节异或递增
4逐字节加递增
5逐字节异或递减
6逐字节加递减

逆运算(验证用):S 盒求逆 + 反向轮函数 + 反向链,CRC 校验通过即还原成功。后面解密真实样本全靠它。

3.4 dt / iv

  • dt:getconf 下发,存 localStorage["ujg3ps2znyw"]
  • iv=4:core 常量 IV_VERSION
  • loadVersion=2.5.4:load.min.js 的 VERSION

四、最大卡点:data 全 False

d/m/p/f/ext 按模块逻辑生成后,get 正常(error:0),check 也正常返回 error:0,但 result 恒为 false。p 从 2 扫到 98(步长 2,同一 token 可重复 check)一个都不中

此时能确定:不是位置问题,是数据本身有问题。但密文看不出问题在哪。

破局:在浏览器手动拖一次,抓通过的 check 请求(result:true),用自研 aes 逆运算把 data 全解密。拿到明文后,三个坑当场现形:

坑 1:p 不取整

真实明文:"77.8125"

前端代码是 parseInt(jigsaw.style.left) / width * 100 + '',直接字符串拼接,没有 round。我之前生成的是 Math.round(...),全灭。

坑 2:f 不是原始轨迹

f 的明文是 47 个浮点数,不是 dx,dy,t,trusted 坐标。前端先把轨迹丢进一个统计模块(module_0x38),算出 47 维特征再拼接:

轨迹点 -> [x序列, y序列, t序列]
-> 速度序列(vx, vy, 合速度) -> 加速度序列(ax, ay, a合)
-> 统计: 数量/去重数/均值/标准差/最小/最大/25分位/75分位

真实样本 f 明文开头:

124,17,0.2042,4.776,142,0,0.8333,0.2133,0.1808,38,0.0625,0.3333,-0.3333,...

直接发坐标 = 风控一眼假。

坑 3:xorEncode 参数顺序

xorEncode(a, b) 输出长度由 b 决定:

xorEncode('数据', 'token')  // 错:输出长度 = token 长度
xorEncode('token', '数据')  // 对:输出长度 = 数据长度

写反后密文长度直接不对,服务端自然不认。

PixPin_2026-08-16_11-37-43

PixPin_2026-08-16_11-38-32

经验:加密参数还原后,必须用真实通过样本对账。 猜出来的逻辑,错在哪个细节根本看不出来。


五、轨迹与 data 生成

真实通过样本透露的轨迹参数:

ext = "2,142"     # mouseDown 次数=2,轨迹点数=142
d   = 50 个采样点  # SAMPLE_NUM=50,等间隔采样
点格式: "dx,dy,t,1"   # 最后一位 trusted=1(真实鼠标)
最后一点: "260,3,1716,1"   # 拖动 260px,耗时 1716ms

对应关系:jigsawLeft=249 时,dragX=260,差值 10.5 来自 restrict() 映射(见第六节)。

生成逻辑(核心代码):

function genTrace(dragX, token) {
  // 140 点 / 1750ms / ease-out / trusted=1
  for (let i = 0; i < 140; i++) {
    const t = Math.round(1750 * i / 139);
    const dx = Math.max(0, Math.round(dragX * (1 - Math.pow(1 - i/139, 3))));
    const dy = Math.round((Math.random() - 0.5) * 6);
    atomTrace.push([dx, dy, t, 1]);
    traceData.push(xorEncode(token, [dx, dy, t, 1].join(',')));
  }
}

function buildData({ token, jigsawLeft, traceData, atomTrace }) {
  const d   = aes(sample(traceData, 50).join(':'));          // 50 采样
  const p   = aes(xorEncode(token, String(jigsawLeft / 320 * 100))); // 不取整
  const f   = aes(xorEncode(token, features(uniqueByTime(atomTrace)).join(','))); // 47 维
  const ext = aes(xorEncode(token, '2,' + traceData.length));
  return JSON.stringify({ d, m: '', p, f, ext });
}

check 通过后拿到服务端 validate,还要再算一次才是给业务方的最终值:

finalValidate = zoneId + '_' + md5(aes(validate + '::' + fp)) + '_v_i_1'
// 例: CN31_90fffa737f70291f7a08893899dc5d3c_v_i_1

六、缺口检测

一开始用拼图小块对背景做模板匹配,匹配到一个无关角落。原因:背景的缺口是被抠空的,拿拼图去匹配空腔,必然失败。

正确做法:拼图 alpha 轮廓 vs 背景 Canny 边缘

mask   = alpha > 0                          # 拼图不透明区域
dist   = distanceTransform(mask)            # 取轮廓(距边界 ≤2px)
outline = (dist <= 2) & (dist > 0)
edges  = Canny(bg)
res    = matchTemplate(edges, outline, TM_CCOEFF_NORMED)
# 取 y 在拼图 bbox 高度范围内的最优 x

用人工通过的图验证:命中 x=251,真实 jigsawLeft=249,差 1px。

PixPin_2026-08-16_11-39-27

restrict() 映射细节:前端滑块有个 restrict(),拼图位移 ≠ 鼠标位移:

jigsawLeft = dragX - 10.5        # 常规区间
jigsawLeft = dragX / 2           # 拖前 21px 内

所以想落到 jigsawLeft=249,鼠标要拖 259.5;轨迹终点的 dx 也必须用 dragX,不是 jigsawLeft


七、结果

验证:

  • check 连续 5/5 result:true
  • 最终 validate 提交看雪 /user-kanxue_login.htm,返回 帐号或密码错误(假账号下即验证码已过)

PixPin_2026-08-16_11-40-15

PixPin_2026-08-16_11-40-33

#1 · 历史 (1)
发帖 2026-08-16 11:45:47 · 快照 2026-08-18 23:15:13

这种帖就是很好,学技术了,能把相关skill打包一下共享就更好了

#2
发帖 2026-08-16 11:46:39 · 快照 2026-08-16 11:46:50

谢谢分享

#3
发帖 2026-08-16 11:47:57 · 快照 2026-08-16 11:48:50

@roiding #1 哈哈哈,这种程度的逆向都不需要skill

#4
发帖 2026-08-16 11:48:10 · 快照 2026-08-16 11:48:50

不错

#5
发帖 2026-08-16 11:49:35 · 快照 2026-08-16 11:50:53

不错的分享 感谢技术大佬的分享

#8
发帖 2026-08-16 13:45:09 · 快照 2026-08-16 13:46:15

#9
发帖 2026-08-16 14:02:08 · 快照 2026-08-16 14:02:22

感谢分享,之前一直没有研究过这块,现在看里面内容还是好多。。

#10
发帖 2026-08-16 14:05:24 · 快照 2026-08-16 14:07:34

@爷 不赖 逆向贴建议看雪也发一份

#12
发帖 2026-08-16 14:40:36 · 快照 2026-08-16 14:41:30

我去了 这我得下下来看

#13
发帖 2026-08-17 08:16:24 · 快照 2026-08-17 20:25:17

哇.可以哦.我就说在这里真能学到东西