案例地址:丰巢登录
郑重声明:本文只做学习研究分享,若有人用文章技术做出不法行为与本人无关,如有侵权联系作者立即删除
找加密参数
丰巢登录会先弹一个滑块。先把它抓下来:按 F12 打开开发者工具,点 Network (网络)面板,勾上 Fetch/XHR (只看接口请求,过滤掉图片那些);然后在页面上把滑块滑一下,面板里就会唰唰列出这期间发的请求。挨个点开看,滑块相关的就两个接口,都在 acs.fcbox.com:
- 取图:
POST /captcha/querySlideImage/{uuid}------ 把滑块图和背景图给你 - 校验:
POST /captcha/checkCode/{uuid}------ 把你滑的结果提交上去
抓包整体长这样(和滑块有关的我都标出来了:①取图、②校验、③校验时才加载的加密代码):

取图接口是明文,POST 一个 {} 就返回一堆东西:
data = { key, clientIp, checkId, pointX, pointY, slideImageUrl, shadeImageUrl }
key:这张图配的一把密码(AES 密钥),一张图一把,不能写死;slideImageUrl:那个能拖的小滑块;shadeImageUrl:带缺口的背景大图;uuid:前端随机生成的一个编号,取图和校验要用同一个。
在 Network 里点开这个取图请求、切到 Response ,就能看到这些参数------红框里的 key 就是这张图的 AES 密钥,后面加密用的就是它:

要还原的是校验接口那个看着像乱码的 body ------它是一整串 Base64(Base64 只是把一堆字节换成能打印的字符,方便传输,本身不是加密)。点开校验请求的 Payload,整屏就是它:

所以事情就两件:里面的内容怎么拼,以及怎么加密。而里面拼的东西,就是 sign(签名)和 track(滑动轨迹)。
定位加密位置
先说"去哪儿看代码",这一步很多新手会卡,我一步一步来。
第一步:认识 Sources。 按 F12 打开开发者工具,点最上面一排的 Sources。这个面板就是"这个网页加载过的所有 JS 文件"的仓库:左边是文件目录树,点哪个文件,右边就显示它的源码。
第二步:滑块代码在哪个文件。 一个网页的 HTML 里,会用 <script src="xxx.js"> 这种标签告诉浏览器"把 xxx.js 下载下来执行"。丰巢登录页的 HTML 里就写了 <script src=".../captcha/slider/fc_slider_v2.0.js">,所以页面一打开,fc_slider_v2.0.js 就已经下载好了 ,在 Sources 左边能直接找到(common.fcbox.com → ... → captcha/slider/fc_slider_v2.0.js)。滑块的主逻辑------画滑块、拼请求------都在这。
第三步:打开并美化。 点开这个文件(懒得翻目录就按 Ctrl+P,输入 fc_slider_v2.0.js 回车打开)。打开后是一整行压缩代码,没法读,点编辑器左下角那个 {} 图标"美化"一下,就变成多行可读了。
第四步:搜关键词定位到关键行。 在代码里按 Ctrl+F 搜 aesKey,就跳到校验前这段:

别被一堆 _0x 名字吓到,逆着读就四步(对应图里红框 ①~④):
_0x58a573 = _0x100f83['md5']:从加密模块里把 md5 函数取出来;_0x5a6b8f = _0x271ca0 + _0x9d8fc6 + _0x31ab21 + _0x44bc33(_0x4c1279):把四段直接拼成一个长串,其中_0x44bc33(_0x4c1279)是把滑动轨迹数组拼成一长条;这四段我用真机对过,依次是clientIp + checkId + uuid + 轨迹串;_0x427682 = _0x58a573(_0x5a6b8f):就是sign = md5(那串);_0xa5e81f = _0x1ad9f1({'data':{'sign':_0x427682,'track':_0x4c1279},'aesKey':_0x39ab99}):把sign、track连同这张图的aesKey交给encrypt,得到密文。
再往下一行就是发请求:data 填这段密文,url 拼 /captcha/checkCode/------你在抓包里看到的那串"乱码 body",就是它。
想看它发出去之前到底带了什么,也可以在 checkCode 上打个 XHR 断点 ,拖一下滑块就停住:右上角 ① 是暂停提示,右边 Scope 里 ② 的 data 就是这次要发的密文,下面 ③ 是 Call Stack 调用栈,一眼看清是谁调的:

更进一步,直接在 {sign, track, aesKey} 这一行下断点,拖滑块停住后,Scope 里每个变量当场就是真值------这是我真机停到的一次:
_0x271ca0 (clientIp) = 111.25.238.195
_0x9d8fc6 (checkId) = DAAA0E65FE4142E9AC216ABC0623EC55
_0x31ab21 (uuid) = 06754300-6f13-4c25-a166-57ebed2f1ed9
_0x4c1279 (track) = Array(30),首 {x:5,y:83,time:...} 尾 {x:150,y:83,time:...}
_0x39ab99 (aesKey) = hwFhkeyTCnkwhtBt ← 16 位,正是取图返回的那把 key
_0x5a6b8f (拼接串) = 111.25.238.195DAAA0E65...(clientIp+checkId+uuid+轨迹串)
_0x427682 (sign) = 559565ff77d07b6fa71c633346034e4d
_0xa5e81f (密文data) = ZCcZ9fALokGEgbMGtyo+DRy0...
# 在暂停的帧里,用页面自己的 md5 函数 _0x58a573 现算一遍:
_0x58a573(_0x5a6b8f) === _0x427682 → true
到这儿,sign = md5(clientIp + checkId + uuid + 轨迹串) 就不是我嘴上说,是断点现场算给你看的。
那 aes.chunk.js 是怎么回事? 真正做加密的 encrypt 不在上面这个文件里,而在另一个小文件 aes.chunk.js。现在网站习惯把 JS 拆成很多小包(叫 chunk),用到哪块才下载哪块 。刚打开登录页时滑块还没弹、还没用到加密,所以 aes.chunk.js 还没下载,Sources 里暂时看不到它------不是没有,是还没来;等滑块真正要加密时它才被拉下来。知道有这回事就行,不用慌。
进到 aes.chunk.js(就是上面总览图里 ③ 那个"用到才加载"的文件),美化后看 encrypt 这块:

别被 _0x 吓到,图里我圈了三处,照着看就懂:
- ① 这个模块导出两个函数:
encrypt和md5(return _0x5b9645就是 encrypt 本体); - ②
_0x210a36 = _0xe93988'n' 这行,是从 crypto-js 里把 AES 模块取出来; - ③ 最后这个
return:把"明文"和"key"都先按Utf8转一下,再AES.encrypt(明文, key, {mode, padding}),末尾.toString()。
翻成人话就是这段(变量名换成你能读的):
function encrypt(payload) {
var data = payload.data; // {sign, track}
var key = CryptoJS.enc.Utf8.parse(payload.aesKey);
var text = typeof data === 'string' ? data : JSON.stringify(data);
return CryptoJS.AES.encrypt(
CryptoJS.enc.Utf8.parse(text), key,
{ mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7 }
).toString();
}
怎么看出来它是标准 AES? 先看写法:AES.encrypt(明文, key, {mode, padding}).toString() 就是 crypto-js 里最标准的调用,参数没有被魔改;toString() 默认就是把密文转成 Base64。但光"看着像"不算数------混淆之后 mode/padding 具体是哪个常量,肉眼看不出,得验。
怎么验(相互证明):
-
在浏览器里真滑一次,抓下校验请求那串密文,再拿同一张图的
key; -
用 Python 的 AES-ECB + 这个
key去解那串密文------能解回正常的{sign, track}JSON ,就说明确实是 AES-ECB(mode 错一点,解出来就是一坨乱码); -
再把解出来的
track按sign = md5(clientIp + checkId + uuid + 轨迹串)重算一遍,跟包里带的sign对一下,看是不是一字不差。
真实跑出来的结果:
# 用 key 解 checkCode 密文,得到明文(节选)
{"sign":"b5e692e202ae88a008b23077a72b34d1","track":[{"x":5,"y":58,"time":1792747684676},{"x":10,"y":58,"time":1792747684676}, ...]}
# 把 sign 重算一遍比对
sign in package : b5e692e202ae88a008b23077a72b34d1
md5 recomputed : b5e692e202ae88a008b23077a72b34d1
MATCH : True
两边一对上,"AES-ECB + PKCS7 + Base64、key 用取图返回的那把、sign 是四段拼接的 md5"------整条链就都实锤了。
补一句人话:AES 是「对称加密」,加密和解密用同一把钥匙;ECB 是它最省事的一种分组模式------不用额外的初始向量,同样的明文块永远得到同样的密文块。丰巢这里就是 ECB。
这里 sign 是「签名」:把几个字段拼一起丢进 md5 压成一小串,服务端用同样的规则再算一遍来核对,你改其中一个字段就对不上。轨迹串就是把每个滑动点的 x、y、时间 直接连成一长条(中间不加逗号、不加空格)。这四段拼接的顺序是固定的,错一个就过不了。
图像处理(找缺口)
能不能过,命门在这一步:滑块要滑到缺口的水平位置,得先从背景图上把缺口找出来。
丰巢这个缺口,不是 把滑块图原样贴回背景,而是背景上有一块比周围暗的、滑块形状的影子------你要找的就是这块"暗影"在哪。
我一开始按"把图贴回去"的思路做模板匹配(模板匹配就是拿一小块图在大图上滑,找最像的位置),结果它经常跑偏到离谱的地方(有次直接给到 x=14,实际缺口在 250 开外,差得没边)。后来换了个更对症的办法:拿滑块的形状在背景上从左往右移,看移到哪个位置时"框里比框外暗得最明显",那里就是缺口;再用一个只看边缘轮廓的方法独立算一遍,两个结果差不多就取平均。
下面这张是实拍的图:左边是滑块小图,右边是背景大图,红框就是程序找出来的缺口。这张里"暗影法"给 256、"轮廓法"给 259,差在容差内,取平均就是 258:

复现加密逻辑
把缺口位置换算成要滑的距离:背景图显示宽度和原始宽度差不多,所以缺口在第几个像素,基本就等于要滑过去多少像素 ,再加一个初始偏移 pointX;拖动有上限(最多 274 像素),每个点的 y 都固定等于 pointY,时间 用毫秒。
然后按 sign = md5(...) 和 AES-ECB 把 payload 拼出来、加密。轨迹也别画得太"完美":匀速一条直线风控一眼看出是机器,所以手动加了点"人味"------起步慢、中间快、快到头再慢下来,中间带一点点上下抖,冲过头一两像素再倒回去对准,每段花的时间也随机一点。
Python 侧加密就两行(pad 是把明文补齐到 16 字节的整数倍,AES 分组要求,补法叫 PKCS7):
cipher = AES.new(key.encode(), AES.MODE_ECB)
base64.b64encode(cipher.encrypt(pad(明文.encode(), 16))).decode()
至于为什么是 ECB 不是 CBC:我把网站真实的 JS 拿到 Node 里跑,喂一段长明文,看密文分块之间有没有前后牵连------没有,就是 ECB。Python 写出来跟它逐字节比,一模一样。
结果
一个 fcbox_slider.py 从头跑到尾,每步都打日志,卡在哪一眼看见。这是我本机刚跑的一次:
==============================================================
蜂巢滑块验证码 · 单文件全链路
==============================================================
[取图] uuid=1b251e4f-48d6-4701-b835-2b17c98b1819 pointX=0 pointY=96
[识别缺口] hole=227 grad_ncc=230 sqdiff_rgb=201 -> gap_x=228
[生成轨迹] 67 点 x: 0 -> 228 过冲 4px ~1.30s
[提交校验] 第 1/3 次 code=400100000 [PASS] 通过
--------------------------------------------------------------
结果 : 通过 [PASS]
响应码 : 400100000
凭证 token : 267E216923AA4107871338AFF54E376B
--------------------------------------------------------------
code=400100000 就是通过,token 是滑块给的凭证,后面登录会用到它。失败重试时注意:响应里 checkId 会换新的,要用新的重新签,别拿旧的。
踩坑
- 定位加密别一上来就搜
CryptoJS:丰巢页面上它只会命中一个不相干的jsencrypt.min.js(那是另一套 RSA 库)。真正能一下定位到加密的,是搜接口名querySlideImage或aesKey; key一张图一把,别写死;uuid取图和校验要用同一个;sign里clientIp + checkId + uuid + 轨迹串的顺序、以及轨迹点数字"直接相连不加分隔",错一处就过不了;- 缺口识别别用"把滑块贴回背景"的思路,它是背景上一块暗影,用贴图的模板匹配会跑偏。
小结
丰巢这点事,加密不难(就是标准 AES-ECB + md5 签名),难在找缺口 ------别被"把滑块贴回去"的惯性想法带偏,它其实是背景上一块暗影子。找对位置基本一把过,我连测 4 次全中。定位的时候也别被文件名骗着去搜 CryptoJS,顺着 aesKey 这条线反而最快。如有问题请及时提出,加油加油。