首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
WEB安全
发新帖
3
3
[原创] 某网站极验逆向 v3.1.2
发表于: 2025-5-16 20:23
5140
[原创] 某网站极验逆向 v3.1.2
YuHuanTin
2025-5-16 20:23
5140
# 前言 jscrambler 混淆器,主要是可见性的问题,一些 unicode 字符的去除,还有 `decodeURI` 里面的内容不能直接解码,得用源代码保留  ## 混淆器手法 逆向该混淆器的时候有一些通用的模板 首先是类似于这样的形式的代码,我们通过查看能够知道 `f3.$_Ct` 是一个函数,通过输入对应的索引就能得出对应的字符串,类似于字符串池,而以下的代码有用的只有 `v507` 和 `v509` 其余的代码都是死代码( `v510` 几乎不会被用到) ```jsx var v507 = f3.$_Ct; var v508 = ["$_HCDg"].concat(v507); var v509 = v508[1]; v508.shift(); var v510 = v508[0]; ``` 没什么用的 `switch`,单步即可 ```jsx function f151(p529) { var v1994 = f3.$_DA()[10][14]; while (v1994 !== f3.$_DA()[8][13]) { switch (v1994) { case f3.$_DA()[8][14]: var v1995; var vV1965 = v1965(42); var vV19652 = v1965(42); for (v1995 = 0; v1995 <= 3; v1995++) { vV1965 += (vV19652 = v1963(37) + (p529 >>> v1995 * 8 & 255)[v1965(339)](16))[v1965(784)](vV19652[v1963(138)] - 2, 2); } return vV1965; break; } } } ``` `270` 索引的字符串为 `prototype` ,而 `f52` 是一个函数,为的是给函数添加新的函数,类似于继承 ```jsx f52[v465(270)][v467(271)] = function f55(p191) { var v539 = f3.$_Ct; var v540 = ["$_HDIJ"].concat(v539); var v541 = v540[1]; v540.shift(); var v542 = v540[0]; if (p191[v541(236)] < 0 || p191[v541(220)](this[v539(244)]) >= 0) { return p191[v541(242)](this[v539(244)]); } else { return p191; } }; f52[v465(270)][v465(294)] = function f56(p192) { var v543 = f3.$_Ct; var v544 = ["$_HEDH"].concat(v543); var v545 = v544[1]; v544.shift(); var v546 = v544[0]; return p192; }; f52[v465(270)][v465(241)] = function f57(p193) { var v547 = f3.$_Ct; var v548 = ["$_HEIt"].concat(v547); var v549 = v548[1]; v548.shift(); var v550 = v548[0]; p193[v549(204)](this[v547(244)], null, p193); }; f52[v467(270)][v467(284)] = function f58(p194, p195, p196) { var v551 = f3.$_Ct; var v552 = ["$_HFDa"].concat(v551); var v553 = v552[1]; v552.shift(); var v554 = v552[0]; p194[v553(213)](p195, p196); this[v551(241)](p196); }; ```  ## API 信息 - <mark class="encrypted">8abK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6H3j5i4y4K6M7r3!0J5N6q4)9J5k6h3u0A6L8r3W2T1K9h3I4A6i4K6u0W2j5$3!0E0i4K6u0r3P5q4)9J5c8Y4m8S2M7%4y4H3L8%4u0@1i4K6u0V1L8r3!0Y4K9h3&6Q4x3V1k6U0j5i4m8@1j5$3S2S2</mark> 该接口获取极验的 token、challenge、gt  获取参数 - <mark class="encrypted">3dcK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6S2M7r3W2Q4x3X3g2Y4k6h3g2@1k6i4y4@1i4K6u0W2j5$3!0E0i4K6u0r3k6$3g2@1i4K6u0W2M7r3S2H3i4K6y4r3</mark> 该接口获取一些必要参数,后面要用到   获取极验图片 - <mark class="encrypted">428K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6K6N6r3q4@1K9h3y4Q4x3X3g2Y4k6h3g2@1k6i4y4@1i4K6u0W2j5$3!0E0i4K6u0r3M7%4c8S2N6r3W2U0i4K6u0r3K9Y4y4Q4x3V1k6U0L8r3W2U0K9#2)9J5k6e0y4Q4x3X3f1I4i4K6u0W2x3W2)9J5k6h3A6K6</mark> <mark class="encrypted">ad4K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6K6N6r3q4@1K9h3y4Q4x3X3g2Y4k6h3g2@1k6i4y4@1i4K6u0W2j5$3!0E0i4K6u0r3M7%4c8S2N6r3W2U0i4K6u0r3K9Y4y4Q4x3V1k6Y4j5%4c8Q4x3X3g2T1y4K6q4S2z5e0l9J5y4K6f1H3z5h3u0U0y4X3u0U0k6X3g2X3z5h3k6U0y4X3p5I4z5e0j5@1x3U0c8X3y4g2)9J5k6h3A6K6</mark> 这两个 `js` 实现了极验的加密(下面详细讲) - <mark class="encrypted">8a8K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6S2M7r3W2Q4x3X3g2Y4k6h3g2@1k6i4y4@1i4K6u0W2j5$3!0E0i4K6u0r3j5h3A6S2P5q4)9J5k6i4m8Z5M7l9`.`.</mark> 该接口用于提交请求  点击验证提交 ## 逆向流程 ## 构造请求包 首先找到提交 `post` 请求的函数  post 提交 调用者上面就是请求的参数,可以很清晰的看到 通过查看,我们可以发现 `gt` `challenge` `lang` 在上述 `get.php` 的接口就可以找到,剩下的参数有 `pt` `client_type` `w`  生成的地方 ### pt 参数 通过搜索 643 这个索引即可找到赋值的地方,它由 `v452` 决定,可能为 `0` 或 `3`  ```jsx var v452 = /Mobi/i[v11(107)](v450[v11(141)]); ``` 我们通过索引 `107` 和 `141` 我们可以知道源代码长这样 ```jsx var v452 = /Mobi/i.test(v450.userAgent); ``` 而其中 v450 为 **Navigator**,是浏览器提供的结构,至此我们可以知道 `pt` 的值代表着 `UA` 字符串中是否 `Mobi` 字样,如果有,则为 `3` ,如果没有则为 `0` ### client_type 参数 在 `pt` 参数附近,见上图,也就是 ```jsx v452 ? v11(683) : v11(660); ``` 翻译一下就是 ```jsx V452 ? 'web_mobile' : 'web' ``` 同理,非 `Mobi` 字样 UA 选择 `'web'` ### w 参数 `w` 是一个组合的参数 ```jsx w: v2021 + v2019 ``` ```jsx var v2019 = vThis25[v1925(751)](); var v2020 = vF7[v1927(374)](vF10[v1927(109)](vO16), vThis25[v1925(750)]()); var v2021 = vO8[v1927(774)](v2020); ``` 分析该三个数据即可 ### v2019 ```jsx $_CABQ: function (p538) { var v2081 = f3.$_Ct; var v2082 = ["$_CBGDt"].concat(v2081); var v2083 = v2082[1]; v2082.shift(); var v2084 = v2082[0]; var v2085 = new vF6()[v2081(374)](this[v2081(750)](p538)); while (!v2085 || v2085[v2083(138)] !== 256) { v2085 = new vF6()[v2081(374)](this[v2081(750)](true)); } return v2085; } ``` 其中主要的是 `new vF6()[v2081(374)](this[v2081(750)](p538));` 内部的函数调用 `$_CACd`,而通过调试我们可以知道它会返回 `v1811` ,它从 `vF9()` 初始化得到   `vF9` 可知它返回一个 16 字符长度的字符串,也就是 AES key,然后该生成的随机 AES key 会通过 RSA 加密,其中构造 `vF6` 的时候会自动加载公钥,然后调用 `encrypt` 函数  通过断点 `setPublic` 我们可以知道它会获取公钥 ```jsx 00C1E3934D1614465B33053E7F48EE4EC87B14B95EF88947713D25EECBFF7E74C7977D02DC1D9451F79DD5D1C10C29ACB6A9B4D6FB7D0A0279B6719E1772565F09AF627715919221AEF91899CAE08C0D686D748B20A3603BE2318CA6BC2B59706592A9219D0BF05C9F65023A21D2330807252AE0066D59CEEFA5F2748EA80BAB81 10001 ``` 这里它虽然写的是这个,但是后面的解析其实是不需要首部的 `00` 字节的,所以我们去掉 `00` 然后转换为 `PEM` 格式公钥 ```jsx -----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDB45NNFhRGWzMFPn9I7k7IexS5 XviJR3E9Je7L/350x5d9AtwdlFH3ndXRwQwprLaptNb7fQoCebZxnhdyVl8Jr2J3 FZGSIa75GJnK4IwNaG10iyCjYDviMYymvCtZcGWSqSGdC/Bcn2UCOiHSMwgHJSrg Bm1Zzu+l8nSOqAurgQIDAQAB -----END PUBLIC KEY----- ``` 通过跟踪 `encrypt` 函数,我们可以从中发现填充的代码,通过动态跟踪我们可以知道这是 `RSAES-PKCS1-v1_5` 填充 ```jsx var v_ = function f91(p240, p241) { var v740 = f3.$_Ct; var v741 = ["$_JBIY"].concat(v740); var v742 = v741[1]; v741.shift(); var v743 = v741[0]; if (p241 < p240[v742(138)] + 11) { if (console && console[v742(65)]) { console[v740(65)](v740(365)); } return null; } var vA5 = []; var v744 = p240[v742(138)] - 1; while (v744 >= 0 && p241 > 0) { var v745 = p240[v742(166)](v744--); if (v745 < 128) { vA5[--p241] = v745; } else if (v745 > 127 && v745 < 2048) { vA5[--p241] = v745 & 63 | 128; vA5[--p241] = v745 >> 6 | 192; } else { vA5[--p241] = v745 & 63 | 128; vA5[--p241] = v745 >> 6 & 63 | 128; vA5[--p241] = v745 >> 12 | 224; } } vA5[--p241] = 0; var v746 = new f42(); var vA6 = []; while (p241 > 2) { vA6[0] = 0; while (vA6[0] == 0) { v746[v740(217)](vA6); } vA5[--p241] = vA6[0]; } vA5[--p241] = 2; vA5[--p241] = 0; return new f44(vA5); }(p239, this[v736(219)][v738(383)]() + 7 >> 3); ``` 至此我们分析完了 v2019 的流程,也就是随机生成 AES key,然后再拿 `RSA` 公钥加密 AES key,返回 `hex` 格式字符串 ### v2020 将 `vO16` 对象序列化,然后调用 `AES` 加密,密钥是刚刚生成的 AES key   跟踪到里面就可以获取 `IV` 是 `0000000000000000` 字符串,然后返回字节数组  v2020 生成的内容 ### v2021 自定义编码函数,类似于 `base64` 但是拓展到了 74 位 ```jsx ()*,-./0123456789:?@ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz~ ``` 这里可以直接把代码扣下来 ```jsx let $_DBe = f3.$_Ct(174); let $_DCz = f3.$_Ct(71); let $_DDr = 7274496; let $_DEA = 9483264; let $_DFp = 19220; let $_DGb = 235; let $_DHo = 24; function $_EAe(p143) { var v387 = f3.$_Ct; var v391 = $_DBe; if (p143 < 0 || p143 >= v391.length) { return v387(71); } else { return v391.charAt(p143); } } function $_ECv(p145, p146) { return p145 >> p146 & 1; } function $_EDt(p147) { var v402 = f3.$_Ct; var vF4 = function (p149, p150) { var vLN09 = 0; for (var v408 = $_DHo - 1; v408 >= 0; v408 -= 1) { if ($_ECv(p150, v408) === 1) { vLN09 = (vLN09 << 1) + $_ECv(p149, v408); } } return vLN09; }; var vV402 = v402(42); var vV4022 = v402(42); for (var v409 = p147.length, vLN010 = 0; vLN010 < v409; vLN010 += 3) { var v410; if (vLN010 + 2 < v409) { v410 = (p147[vLN010] << 16) + (p147[vLN010 + 1] << 8) + p147[vLN010 + 2]; vV402 += $_EAe(vF4(v410, $_DDr)) + $_EAe(vF4(v410, $_DEA)) + $_EAe(vF4(v410, $_DFp)) + $_EAe(vF4(v410, $_DGb)); } else { var v411 = v409 % 3; if (v411 == 2) { v410 = (p147[vLN010] << 16) + (p147[vLN010 + 1] << 8); vV402 += $_EAe(vF4(v410, $_DDr)) + $_EAe(vF4(v410, $_DEA)) + $_EAe(vF4(v410, $_DFp)); vV4022 = $_DCz; } else if (v411 == 1) { v410 = p147[vLN010] << 16; vV402 += $_EAe(vF4(v410, $_DDr)) + $_EAe(vF4(v410, $_DEA)); vV4022 = $_DCz + $_DCz; } } } return { res: vV402, end: vV4022 }; } function $_EFu_EncodeAESBytes(p152) { var v421 = $_EDt(p152); return v421.res + v421.end; } ``` --- ## 构造数据包 至此,我们能够构造出请求包 `vO18` 了,但是别忘了,AES 加密的内容还不知道是什么,也就是 `vO16` ,`vO16` 在函数头部进行定义  其中 `lang` `pic` 已知,在 `get.php` 中可以找到 `a` `passtime` 是传入的点击位置与消耗时间 其余需要构造 ### tt 参数 ```jsx tt: function (p476, p477, p478) { var v1932 = f3.$_Ct; var v1933 = ["$_CADIZ"].concat(v1932); var v1934 = v1933[1]; v1933.shift(); var v1935 = v1933[0]; if (!p477 || !p478) { return p476; } var v1936; var vLN052 = 0; var vP476 = p476; var v1937 = p477[0]; var v1938 = p477[2]; var v1939 = p477[4]; while (v1936 = p478[v1932(784)](vLN052, 2)) { vLN052 += 2; var vParseInt = parseInt(v1936, 16); var v1940 = String[v1932(181)](vParseInt); var v1941 = (v1937 * vParseInt * vParseInt + v1938 * vParseInt + v1939) % p476[v1932(138)]; vP476 = vP476[v1934(784)](0, v1941) + v1940 + vP476[v1932(784)](v1941); } return vP476; }(v1931, v1929[v1927(729)], v1929[v1927(236)]) ``` 其中三个参数分别为:鼠标移动点位数据,`get.php` 提供的 `c` ,`get.php` 提供的 `s`,然后通过该函数能够得出编码的轨迹数据 ### ep 参数 调用以下函数,其中 `this[v2086(617)][v2088(630)]` 为点位数据中的点击数据,见下 `v` 是极验的版本 `3.1.2` `$_FG` 为 `touchEvent` 的值 `me` 为 `mouseEvent` 的值 `tm` 为浏览器中 `window.performance.timing` 的数据 ```jsx $_CAAe: function () { var v2086 = f3.$_Ct; var v2087 = ["$_CBGIo"].concat(v2086); var v2088 = v2087[1]; v2087.shift(); var v2089 = v2087[0]; console.log(`let dots = JSON.parse('${JSON.stringify(this[v2086(617)][v2088(630)])}')`); return { ca: this[v2088(617)] && this[v2086(617)][v2088(630)] || v2086(42), v: v2086(759), $_FG: vV1612[v2086(614)], me: vV1612[v2086(648)], tm: new f129()[v2086(731)]() }; } ``` **点击数据(注意该数据是所有点击的位置,而传入参数中的点击只有验证图像中的点击位置)** 点击数据的构造由该函数添加,监听 `click` 事件,`p577` 第一个参数是元素,第二个参数是点击事件,其中这几个值的对应的元素是这样的 ```jsx vThis44[v2487(600)][v2489(587)](v2487(453), function (p577) { var v2494 = f3.$_Ct; var v2495 = ["$_CFDIW"].concat(v2494); var v2496 = v2495[1]; v2495.shift(); var v2497 = v2495[0]; var v2498 = p577[v2496(413)][v2496(942)] || p577[v2496(413)][v2496(998)]; var v2499 = v2492(v2496(719))[v2494(112)]; var v2500 = v2492(v2496(737))[v2496(112)]; var v2501 = v2492(v2496(762))[v2494(112)]; var v2502 = v2492(v2496(748))[v2494(112)]; var v2503 = vThis44[v2496(736)](v2494(832))[v2494(112)]; var vO21 = { x: p577[v2496(509)](), y: p577[v2496(567)]() }; if (f156(v2503, v2498)) { vO21[v2496(278)] = 1; } else if (f156(v2502, v2498)) { vO21[v2496(278)] = 3; } else if (f156(v2500, v2498)) { vO21[v2494(278)] = 4; } else if (f156(v2499, v2498)) { vO21[v2496(278)] = 5; } else if (f156(v2501, v2498)) { vO21[v2494(278)] = 6; } else { vO21[v2494(278)] = 0; } if (v2496(865) !== v2491[v2496(744)]) { var v2504 = v2498[v2496(436)] || v2494(42); if (v2504[v2496(191)](vV11 + v2496(990)) >= 0) { var v2505 = v2504[v2496(191)](vV11 + v2496(813)) >= 0; vO21[v2496(278)] = v2505 ? 1 : 2; } if (v2504[v2496(191)](vV11 + v2494(969)) >= 0) { vO21[v2494(278)] = 2; } } vThis44[v2496(901)](vO21); vThis44[v2494(630)][v2494(108)](vO21); vThis44[v2494(954)](); }); ``` `v2498` 是当前点击的元素  `vThis44[v2496(901)](vO21);` 添加时间差 --- 所以点击的结构体是这样的 `t` 是点击的元素,`1` 为点击验证码图像,`3` 为点击提交按钮(最后一个一定是 `3`) `dt` 为距离上一个 `pointerdown` 的时间差(毫秒) ```jsx [{"x":943,"y":365,"t":1,"dt":3881}, {...}] ``` ## 附加额外字段 除此之外还有一个 `vO17` 对象需要构造,但是直接使用 `gct.js` 即可,然后 `vO16` 会附加上 `vO17` 的部分数据(也可以直接给定值) `"h9s9"` 为 `"1816378497"`  gct.js 中含有反调试,建议是直接给固定结果 1803797734,该值会永远计算出 1816378497 最后 `vO16` 还会使用 `gt` + `challenge` + `passtime` 字符串进行 `md5` 计算得出 `rp` 字段 ### 结果 
登录后可查看完整内容
传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!
#其他
收藏
・
3
点赞
・
3
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
mb_tlilmfio
为你点赞!
2025-10-14 17:57
道友请留步
你的帖子非常有用,感谢分享!
2025-7-14 11:35
git_54853450196893
这个讨论对我很有帮助,谢谢!
2025-7-8 12:03
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
1
)
七浪
雪 币:
32
活跃值:
(115)
能力值:
( LV2,RANK:10 )
在线值:
发帖
1
回帖
24
粉丝
3
关注
私信
七浪
2
楼
感谢分享,正在对极验三代滑块的 w 和缺口距离。
2026-9-30 08:31
0
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
YuHuanTin
7
发帖
9
回帖
40
RANK
关注
私信
他的文章
[原创]一款可分享密码的解压缩 app 分析学习
28228
[原创]Wechat H5 DevTools
4537
[原创]趣智校园 app 分析
6804
[原创][首发] b 站 drm 视频流程简析
13610
[原创] 某 frp 反调试
1367
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部