凯里做网站的公司,动漫网站设计模板,苏州集团网站制作公司,邯郸网站制作外包文章目录 1. 写在前面2. 接口分析3. w逆向分析4. JSON参数分析5. 距离识别6. RSA纯算还原7. AES纯算还原 【#x1f3e0;作者主页】#xff1a;吴秋霖 【#x1f4bc;作者介绍】#xff1a;擅长爬虫与JS加密逆向分析#xff01;Python领域优质创作者、CSDN博客专家、阿里云… 文章目录 1. 写在前面2. 接口分析3. w逆向分析4. JSON参数分析5. 距离识别6. RSA纯算还原7. AES纯算还原 【作者主页】吴秋霖 【作者介绍】擅长爬虫与JS加密逆向分析Python领域优质创作者、CSDN博客专家、阿里云博客专家、华为云享专家。一路走来长期坚守并致力于Python与爬虫领域研究与开发工作 【作者推荐】对爬虫领域以及JS逆向分析感兴趣的朋友可以关注《爬虫JS逆向实战》《深耕爬虫领域》 未来作者会持续更新所用到、学到、看到的技术知识包括但不限于各类验证码突防、爬虫APP与JS逆向分析、RPA自动化、分布式爬虫、Python领域等相关文章 作者声明文章仅供学习交流与参考严禁用于任何商业与非法用途否则由此产生的一切后果均与作者无关如有侵权请联系作者本人进行删除 1. 写在前面 极验相关的验证码网上公开的教程很多这也是每一位爬虫工程师职业生涯中几乎都会遇到的就跟瑞数一样总有一天你会在职场与其打交道也是很多学习Web逆向协议过验证的小伙伴必攻克练手的必经之路。前段时间作者也是看了一眼比较抵触的多个验证码系列写的验证码相关的文章比较少。类型可以说是五花八门复杂的要去训练没有时间去折腾难的也不会。看也就是看下JS层面的一些加密这里我们也来分析一下它的流程适合新手小伙伴参考练习 分析网站 6Ieq5bex5om5LiA5Liq77yM5b2T5L2g5bCd6KV6Kej56CB55qE6LZ5LiA5Yi744CC5oOz5ZGK6KJ5L2g5Yir5Y235LqG 2. 接口分析
整个验证的流程比较简洁两个接口一个load一个verify。先看一下load接口请求参数与响应数据如下 {status: success,data: {lot_number: 80ff732c046e471286a596376f23e6ba,captcha_type: slide,slice: pictures/v4_pic/slide_2024_09_02/e9d1dec400/slide/da4cb40154354b98b0aabc59a516407b.png,bg: pictures/v4_pic/slide_2024_09_02/e9d1dec400/bg/da4cb40154354b98b0aabc59a516407b.png,ypos: 88,arrow: arrow_1,js: /js/gcaptcha4.js,css: /css/gcaptcha4.css,static_path: /v4/static/v1.8.9-2c5e49,gct_path: /v4/gct/gct4.5a2e755576738ba0499d714db4f1c9e0.js,show_voice: false,feedback: ,logo: false,pt: 1,captcha_mode: adaptive,guard: false,check_device: true,language: en-us,lang_reverse: false,custom_theme: {_style: flat,_color: hsla(166, 100%, 35%, 1),_gradient: linear-gradient(180deg, hsla(166, 100%, 35%, 1) 0%, hsla(166, 100%, 35%, 1) 100%),_hover: linear-gradient(180deg, hsla(166, 100%, 30%, 1) 0%, hsla(166, 100%, 30%, 1) 100% ),_brightness: system,_radius: 4px},pow_detail: {version: 1,bits: 0,datetime: 2025-06-14T14:46:04.90850608:00,hashfunc: md5},payload: AgFD8gWUUuHFx-XvpP7J2eXmFGG988RJA9XWIifIhVfD_3urF6TwX10q6TrWNlmoLrmnDZXhh-Ds0tvYuF6iz5alWQDqnpl0rqqdGpTnlPBV0BX8BWJ5g0YFxJt1IOoQVcRgUKRcBVWhr05ylkkJZXmzCjWL1dDhSlKM126f2CZbUhZx485twyTTIgg6TxkHp6RFiEexFFTfAtnhmMzgKU23kd_SmUIpkhKKF4bwPjaDya1ARBt-LMTEJSFl0XZKUXBONNi2GkE0BIbNVEDSLi4QK7Zj55XdDCTUuBkmYr5YxPXUERlbVg3UZyx0Y9EXEHMgaUL7CySS6wrF3_lx4x_5wMrC7FQY9RMZ_mIGUpjk3HIc5OQIQlOZEVDlvEIm6dwESxLJZha5UI92v7w-w9ceoaQGo1oBJ1uqRmPxQsHpmDS77OCNf8r2ymn4nt-mtL4ee2U0yyMX1py5BM1gNWR8kAEVvFWcwwWObMFjMtWSuP4yT193BgbmfCwK_N1dnpCcMCVppkdkfgUQ_Eay_oiTfpR17q7G1gGG52U-NLWM5wgGVTNzmw83Js2pPfl9zQB3By9VIzmwvNqJzsQUg3H_2WyII9YZ5BPAxqe6j3W3-uL1UCyrx4UoYY-Pp5JugjrxQ26vBWE7B5cWc3poPHwTa5nXmyiS3QbZYc_qw3K_YyF96yTL2AVq2TzR6DOiqM1cb6HloK2rVD8mRquqbXXA7rvdKTlYswSxJXSDD6K-bg8CMQ_br4AnxmfC1U_blfAwWSZnqSyoHb9g8X3ejeeW1qxkEYIAcG6bmKhb8gMWTSV1I3WtU7sBLqc5o4O4Qm4q3Xl3gN8bPsh4RSQXJ5M8QudVdBWid0iR5afT2-aL0ZAGOb7a1ooFhrpFndEkT7cHFPG-mMp-HHjY8giZwK8minfPqGKxdOfsQbUqnP8,process_token: 233085ee7f960fae79d07dcd40515833e0367c2e4edc1ba4bc5b530dc5e7600b,payload_protocol: 1}
}上面JSON数据是load接口的响应包含了一些图片的信息跟后续验证接口二次携带请求的参数字段。然后看一下verify验证接口提交的参数如下所示 标记出来的基本都是load接口响应的参数都是后续提交验证过程中要拿来使用的唯一要逆向解决的参数就是w所以我们需要知道这个w的明文数据是什么然后通过什么加密方式出来的
3. w逆向分析
首先需要定位这个w生成的位置搜索w:、从下往上看栈两三个、搜其他特征明显的请求参数均可以帮助我们定位到相关位置处如下所示 现在都这么抽象了么关键的一些变量、函数、参数那些看起来使用了Unicode字符来替代。这种如果觉得硬看费劲的话可以使用AST来辅助还原还原流程大致如下所示 还原这个混淆JS的关键策略就是构建映射表根据实际的分析情况去补充跟过滤
接下来我们来看w参数是怎么生成的把生成这个参数的这一行代码拿下来如下所示
_ᖆᕶᖙᖁ (0,_ᕴᖁᕹᖄ[_ᖃᕾᕶᖂ(67)])(_ᖁᖉᕿᖈ[_ᕹᖃᕶᕿ(67)][_ᕹᖃᕶᕿ(559)](_ᖘᖄᖁᕿ), _ᕴᖙᖄᖘ)_ᖆᕶᖙᖁ就是加密之后的结果_ᖘᖄᖁᕿ是加密的明文JSON数据然后做了一个JSON.stringify的操作如下所示 这里我们核心看_ᕴᖁᕹᖄ[_ᖃᕾᕶᖂ(67)]这个函数我们直接跟进去看看是什么通过调用它传递了JSON数据生成的w值 这里我们把上面跳转到的核心JS代码拿下来再联动网页调试来分析一下流程如下所示
_ᖘᖄᖁᕿ[_ᖃᕾᕶᖂ(67)] function _ᖆᕶᖙᖁ(_ᖘᖄᖁᕿ, _ᖁᖆᕸᕸ) {var _ᖃᕾᕶᖂ _ᖘᖈᖙᖃ.$_CL, _ᖁᕷᕹᖄ [$_DCAAk].concat(_ᖃᕾᕶᖂ), _ᕹᖃᕶᕿ _ᖁᕷᕹᖄ[1];_ᖁᕷᕹᖄ.shift();var _ᖃᕹᖄᖚ _ᖁᕷᕹᖄ[0];var _ᖆᖘᕶᕹ _ᖁᖆᕸᕸ[_ᖃᕾᕶᖂ(439)];if (!_ᖆᖘᕶᕹ[_ᖃᕾᕶᖂ(604)] || _ᖃᕾᕶᖂ(183) _ᖆᖘᕶᕹ[_ᕹᖃᕶᕿ(604)])return _ᕴᖙᖄᖘ[_ᕹᖃᕶᕿ(67)][_ᖃᕾᕶᖂ(921)](_ᖘᖄᖁᕿ);var _ᖀᕷᖚᖉ (0,_ᕷᕸᖉᕷ[_ᕹᖃᕶᕿ(93)])(), _ᖂᖗᕿᖁ new (_ᕷᕸᖉᕷ[_ᕹᖃᕶᕿ(30)])([_ᖃᕾᕶᖂ(873), _ᕹᖃᕶᕿ(898)]), _ᖄᖈᕸᖃ {1: {symmetrical: _ᕿᕶᖆᖃ[_ᕹᖃᕶᕿ(67)],asymmetric: new (_ᖂᕿᕷᖀ[_ᖃᕾᕶᖂ(67)])},2: {symmetrical: new (_ᖄᕵᕸᖁ[_ᖃᕾᕶᖂ(67)])({key: _ᖀᕷᖚᖉ,mode: _ᕹᖃᕶᕿ(926),iv: _ᕹᖃᕶᕿ(981)}),asymmetric: _ᖂᕸᖄᕿ[_ᕹᖃᕶᕿ(67)]}};if (_ᖂᖗᕿᖁ[_ᖃᕾᕶᖂ(123)](_ᖆᖘᕶᕹ[_ᖃᕾᕶᖂ(604)])) {var o _ᖃᕾᕶᖂ(873) _ᖆᖘᕶᕹ[_ᖃᕾᕶᖂ(604)], a _ᖆᖘᕶᕹ[_ᖃᕾᕶᖂ(604)], _ _ᖄᖈᕸᖃ[a][_ᕹᖃᕶᕿ(938)][_ᖃᕾᕶᖂ(911)](_ᖀᕷᖚᖉ);while (o (!_ || 256 ! _[_ᖃᕾᕶᖂ(64)]))_ᖀᕷᖚᖉ (0,_ᕷᕸᖉᕷ[_ᖃᕾᕶᖂ(93)])(),_ (new (_ᖂᕿᕷᖀ[_ᕹᖃᕶᕿ(67)]))[_ᖃᕾᕶᖂ(911)](_ᖀᕷᖚᖉ);var u _ᖄᖈᕸᖃ[a][_ᖃᕾᕶᖂ(934)][_ᖃᕾᕶᖂ(911)](_ᖘᖄᖁᕿ, _ᖀᕷᖚᖉ);return (0,_ᕷᕸᖉᕷ[_ᖃᕾᕶᖂ(5)])(u) _}}}它这个w的值由两段密文拼接组成的先看_ᖄᖈᕸᖃ[a][_ᕹᖃᕶᕿ(938)][_ᖃᕾᕶᖂ(911)](_ᖀᕷᖚᖉ)这个位置的代码生成的是第一段。通过对一个长度16位的字符串进行了一个encrypt操作得到的如下所示 好这里看看encrypt对应的JS先解决第一段的密文。再去往下进行分析跳转后直接拿下来代码如下所示
...
_ᖂᖗᕿᖁ[_ᖁᖆᕸᕸ(55)][_ᖁᖆᕸᕸ(1028)] function _ᖆᕶᖙᖁ(_ᖘᖄᖁᕿ, _ᖁᖆᕸᕸ) {var _ᖃᕾᕶᖂ _ᖘᖈᖙᖃ.$_CL, _ᖁᕷᕹᖄ [$_DGHAO].concat(_ᖃᕾᕶᖂ), _ᕹᖃᕶᕿ _ᖁᕷᕹᖄ[1];_ᖁᕷᕹᖄ.shift();var _ᖃᕹᖄᖚ _ᖁᕷᕹᖄ[0];null ! _ᖘᖄᖁᕿ null ! _ᖁᖆᕸᕸ 0 _ᖘᖄᖁᕿ[_ᖃᕾᕶᖂ(64)] 0 _ᖁᖆᕸᕸ[_ᕹᖃᕶᕿ(64)] ? (this[_ᖃᕾᕶᖂ(96)] function _ᖆᕶᖙᖁ(_ᖘᖄᖁᕿ, _ᖁᖆᕸᕸ) {var _ᖃᕾᕶᖂ _ᖘᖈᖙᖃ.$_CL, _ᖁᕷᕹᖄ [$_DGHFu].concat(_ᖃᕾᕶᖂ), _ᕹᖃᕶᕿ _ᖁᕷᕹᖄ[1];_ᖁᕷᕹᖄ.shift();var _ᖃᕹᖄᖚ _ᖁᕷᕹᖄ[0];return new b(_ᖘᖄᖁᕿ,_ᖁᖆᕸᕸ)}(_ᖘᖄᖁᕿ, 16),this[_ᖃᕾᕶᖂ(943)] parseInt(_ᖁᖆᕸᕸ, 16)) : console console[_ᖃᕾᕶᖂ(370)] console[_ᕹᖃᕶᕿ(370)](_ᕹᖃᕶᕿ(1130))
}...省略下一个断刷新后可以看到上面函数先是接受了一个16位的那个密文然后_ᖘᖄᖁᕿ参数的值是一个很长的十六进制字符串256字节2048位直接丢给GPT把参数值一并提交它会提示我们这通常是一个RSA公钥的模数n
_ᖁᖆᕸᕸ参数的值10001十进制公钥指数就算我们不进行AST还原通过作用域结合控制台甚至是鼠标查值的方式也都是也可以看到一些特征的JS通过new b(_ᖘᖄᖁᕿ, _ᖁᖆᕸᕸ)创建了一个加密对象b可能是RSA的构造函数接着parseInt(_ᖁᖆᕸᕸ, 16)将公钥指数转换为数值
看到有console相关的信息输出鼠标扫了一下更加证实了上面的加密方式猜测如下所示 另外说一下这个16位的key如何生成的返回的4个e()相加跟进去看看e是什么如下所示 上面e方法实现了返回一个随机生成四位16进制字符串.toString(16)将整数转为16进制字符串substring(1)再截取字符串的第二个字符到末尾得到一个四位16进制字符串65536相当于16^4即四位16进制数的最大值FFFF1
return (65536 * (1 Math.random()) | 0).toString(16).substring(1);# 示例
Math.random() // 0.74321
1 0.74321 // 1.74321
65536 * 1.74321 // 114245.12056
114245 | 0 // 114245 (十进制)
(114245).toString(16) // 1bdc5
1bdc5.substring(1) // bdc5第一段密文分析完可以成功拿到_的值接下来根据最初贴出来的JS继续往下分析u这个大数组是怎么来的原始代码如下
var u _ᖄᖈᕸᖃ[a][_ᖃᕾᕶᖂ(934)][_ᖃᕾᕶᖂ(911)](_ᖘᖄᖁᕿ, _ᖀᕷᖚᖉ);u这里通过调用了_ᖃᕾᕶᖂ(911)的encrypt传了两个参数_ᖘᖄᖁᕿ是文章开始的那个转成字符串的大JSON_ᖀᕷᖚᖉ则是上面复现的16位key跟进去发现特征跟前面cbc一样并且iv也是一样。走了一个AES的加密操作如下所示 可以看到这个流程它走完AES后返回的是一个长数组即u然后往下再做了一个16进制的字符串转换操作得到最后w参数的另一段值然后把RSA加密得到的那一段短值拼接到u出来这串长值后面得到完整的一个w加密值如下所示 4. JSON参数分析
至此上面关于w参数的整体加密计算流程就分析梳理出来了然后接下来回过头来需要分析一下参与加密的那个大JSON因为貌似涉及的字段看起来不少这里我们将它拿出来分析一下如下所示
{pow_sign : a689bc86ae4eee78c2cf11a946b1b741,ep : 123,passtime : 796,biht : 1426265548,pow_msg : 1|0|md5|2025-06-14T19:18:12.04883508:00|4f70e8a42240f8f809bea8382e738e53|df5ed6e3f65e49709e9dcbd4d595f199||a6ae0baf4ca6c7d1,lot_number : df5ed6e3f65e49709e9dcbd4d595f199,em : {wd : 1,sc : 0,ek : 11,nt : 0,ph : 0,cp : 0,si : 0},geetest : captcha,setLeft : 146,5ed6e3f6 : e9dcbd,userresponse : 147.1369191209607,device_id : ,lang : zh,w22T : 72PZ
}这里主要分析一下pow_sign、pow_msg、userresponse、setLeft以及那个5ed6e3f6:e9dcbd键值对参数先来看pow_msg它由两部分拼接如下所示 前面的那个长字符串用拼接的可以全局搜索这个参数下一个断找初始化的地方很明显还是拿的load接口请求的参数加响应的参数拼起来的如下所示 上面三张图就是整个一次滑块加载出来的参数值拼接出来的
_ᖀᖙᕵᖀ第一个大长串|0|md5|2025-06-14T22:48:45.54930908:00|取自于loiad接口的响应参数pow_detail中四个参数后面追第一个32位字符串取值于请求参数的captcha_id字段第二个32字符串取值与响应参数内的lot_number字段
再看第二部分的h是怎么来的往上看定义的地方直接点进去又来到了上面我们分析的16位key生成的e函数处如下所示 那么看样子走的同样的逻辑至此pow_msg参数分析完毕。接着来看pow_sign参数一样的32字符跟进去看到md5的信息。直接在线工具验证证实就是对pow_msg的值做了一个md5得到了pow_sign如下所示 接下来我们先不看那两个数字结果相关的字段先分析那个key value的字段这个也是动态生成的。回到最开始我们分析w位置的地方往上一点点就能看到这个动态参数的生成这里多刷新了几次发现也不是加密生成的还是对参数字段的值做的索引取值拿到的如下所示 如上取的是load接口中返回的lot_number字段的值索引的规则目前看是下面这样子来对应的它这个取下标的规则挂到了window下面最好动态获取 最后剩下三个数字相关的参数字段来看看如何来的。setLeft是滑块的距离userresponse在滑块距离的数值上进一步做了一个运算。如下所示 5. 距离识别
最后我们只需要对滑块的缺口的距离进行识别拿到setLeft参数的值基本就完成了滑块识别代码实现如下仅供参考
import cv2
import numpy as npdef get_slider_offset(background_img_bytes, slider_img_bytes):识别滑块缺口在背景图中的X坐标。:param background_img_bytes: 背景图片的二进制数据:param slider_img_bytes: 滑块缺口图片的二进制数据:return: 缺口的X坐标# 解码图像为灰度图bg_gray cv2.imdecode(np.frombuffer(background_img_bytes, np.uint8), cv2.IMREAD_GRAYSCALE)slider_gray cv2.imdecode(np.frombuffer(slider_img_bytes, np.uint8), cv2.IMREAD_GRAYSCALE)# 裁剪滑块图像只保留有效区域像素值 200ys, xs np.where(slider_gray 200)slider_crop slider_gray[min(ys):max(ys), min(xs):max(xs)]# 边缘检测bg_edge cv2.Canny(bg_gray, 100, 200)slider_edge cv2.Canny(slider_crop, 100, 200)result cv2.matchTemplate(cv2.cvtColor(bg_edge, cv2.COLOR_GRAY2RGB),cv2.cvtColor(slider_edge, cv2.COLOR_GRAY2RGB),cv2.TM_CCOEFF_NORMED)_, _, _, top_left cv2.minMaxLoc(result)h, w slider_crop.shapecv2.rectangle(bg_gray, top_left, (top_left[0] w, top_left[1] h), (0, 0, 255), 2)cv2.imwrite(distinguish.jpg, bg_gray)return top_left[0]
6. RSA纯算还原
这里我们根据上面的分析先对第一部分的16位key的RSA加密使用Python进行还原还原实现代码如下所示
import base64
import binascii
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5, PKCS1_OAEPdef load_public_key(pem_keyNone, modulusNone, exponentNone):if pem_key:cleaned pem_key.replace(\n, ).replace( , )if ----- in cleaned:cleaned [part for part in cleaned.split(-----) if len(part) 25][0]key_bytes base64.b64decode(cleaned)return RSA.importKey(key_bytes)elif modulus and exponent:n int(modulus, 16) if isinstance(modulus, str) else moduluse int(exponent, 16) if isinstance(exponent, str) else exponentreturn RSA.construct((n, e))raise ValueError(必须提供 PEM 公钥或 modulus exponent)def get_cipher(key_obj, padding_mode: int):scheme {1: PKCS1_OAEP.new,2: PKCS1_v1_5.new}if padding_mode not in scheme:raise ValueError(padding_mode 只支持 1OAEP或 2v1_5)return scheme[padding_mode](key_obj)def get_chunk_size(key_obj):return int(key_obj.size_in_bits() / 1024 * 100)def encrypt_bytes(plaintext: str, *, pem_keyNone, modulusNone, exponentNone, padding_mode1) - bytes:key_obj load_public_key(pem_keypem_key, modulusmodulus, exponentexponent)cipher get_cipher(key_obj, padding_mode)chunk_size get_chunk_size(key_obj)encrypted_data bfor offset in range(0, len(plaintext), chunk_size):segment plaintext[offset:offset chunk_size].encode()encrypted_data cipher.encrypt(segment)return encrypted_datadef encrypt_to_base64(plaintext: str, **kwargs) - str:return base64.b64encode(encrypt_bytes(plaintext, **kwargs)).decode()def encrypt_to_hex(plaintext: str, **kwargs) - str:return binascii.hexlify(encrypt_bytes(plaintext, **kwargs)).decode()def encrypt_to_utf8(plaintext: str, **kwargs) - str:return encrypt_bytes(plaintext, **kwargs).decode(utf-8, errorsignore)modulus (00C1E3934D1614465B33053E7F48EE4EC87B14B95EF88947713D25EECBFF7E74C7977D02DC1D9451F79DD5D1C10C29ACB6A9B4D6FB7D0A0279B6719E1772565F09AF627715919221AEF91899CAE08C0D686D748B20A3603BE2318CA6BC2B59706592A9219D0BF05C9F65023A21D2330807252AE0066D59CEEFA5F2748EA80BAB81
)
exponent 10001# key动态传递
ciphertext encrypt_to_hex(189c7043fd718369, modulusmodulus, exponentexponent, padding_mode2)
print(ciphertext)7. AES纯算还原
这里我们根据上面对u的分析进行AES加密算法还原采用的CBC模式然后iv上面几个0固定的key的话跟参与RSA加密的时候用的一致算法实现如下所示
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import binascii
import secrets
import stringdef encrypt_aes_cbc(plaintext: str, key_str: str, iv_str: str 0000000000000000) - str:使用 AES-CBC 模式对字符串加密并返回 hex 字符串。:param plaintext: 原始明文字符串:param key_str: 16 字节长度的密钥字符串:param iv_str: 16 字节长度的初始向量默认全0:return: 加密后的十六进制字符串key key_str.encode(utf-8)iv iv_str.encode(utf-8)# 初始化加密器cipher AES.new(key, AES.MODE_CBC, iv)# 对明文进行 PKCS7 填充padded pad(plaintext.encode(utf-8), AES.block_size)# 加密并转换为 hex 字符串返回encrypted_bytes cipher.encrypt(padded)return binascii.hexlify(encrypted_bytes).decode(utf-8)# 示例调用
data_to_encrypt {setLeft:146,passtime:796,userresponse:147.1369191209607,device_id:,lot_number:df5ed6e3f65e49709e9dcbd4d595f199,pow_msg:1|0|md5|2025-06-14T19:18:12.04883508:00|4f70e8a42240f8f809bea8382e738e53|df5ed6e3f65e49709e9dcbd4d595f199||a6ae0baf4ca6c7d1,pow_sign:a689bc86ae4eee78c2cf11a946b1b741,geetest:captcha,lang:zh,ep:123,biht:1426265548,w22T:72PZ,5ed6e3f6:e9dcbd,em:{ph:0,cp:0,ek:11,wd:1,nt:0,si:0,sc:0}}
aes_key b9acf19ce1a635a9 encrypted_hex encrypt_aes_cbc(data_to_encrypt, aes_key)
print(encrypted_hex)
最后整个关于极验4滑块的协议验证流程就差不多到这里结束了找了一个套极验4的网站测试了一下。如下所示