某验点选验证码分析
声明
本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关. 本文章未经许可禁止转载,禁止任何修改后二次传播,擅自使用本文讲解的技术而导致的任何意外,作者均不负责,若有侵权,请联系作者立即删除.
链接: aHR0cHM6Ly93d3cuYmlsaWJpbGkuY29tLw==
流程分析
尝试点击页面的登录会触发点选验证码, 随便点几下触发验证流程.
从 devtools 中可以分析得到整个验证码的验证流程如下:
-
首先访问 https://passport.bilibili.com/x/passport-login/captcha 拿到极验的 gt 和 challenge 参数, 用于后续的验证.
-
然后根据 gt 参数访问 https://api.geetest.com/gettype.php 获取并加载当前版本的 js 代码.
-
访问https://api.geetest.com/get.php 获取一些基本信息, 校验的服务器 api 地址, c, s 参数等, 可以看到访问时带了一个 w 参数, 暂时不知道生成逻辑.
-
访问 https://api.geetest.com/ajax.php 接口拿到验证码校验类型, 该接口同样有 w 参数.
-
再次访问 https://api.geetest.com/get.php , 这次请求带上了更多参数, 包括 w, 验证码类型, api_server 等, 返回的数据也带上了验证码图片和新的 c,s 参数.
-
最后请求 https://api.geetest.com/ajax.php 校验验证码, 主要参数是 gt, challenge 和 w, 得到验证结果.
通过对这个流程分析发现主要参数就是 w, 因此要跟踪下三个 w 参数的请求, 看下各自的 w 生成逻辑是什么.
第一次 w 生成逻辑分析
通过查看请求的调用栈可以知道第一个 w 是由 fullpage.xxx.js 生成的.
在请求调用栈上打个断点, 找下 w 参数在哪里生成.
可以看到代码已经经过混淆, 利用ast简单去掉字符串替换和 unicode 编码, override content 之后继续分析.
在代码中搜索"w", 找到 5 处 w 的生成逻辑, 5 处都打下断点后重新刷新.
打下断点再次刷新后停在了其中一处断点, 可以知道第一个 w 由 i+r 组成.
1.1 r 参数分析
r 是由 t["$_CCGw"]函数得到, 往下跟这个函数的逻辑.
核心代码: new X()["encrypt"](this["$_CCHU"](e))
其中this["$_CCHU"]
函数如下图:
可以知道该函数作用是生成 aes key, 如果已存在则使用已存在的 key, 所以这个 r 参数应该是保存加密的 aes key. 跟一下 encrypt 函数看下是什么加密算法.
从一些关键词判断应该是 RSA 算法, 返回 16 进制字符串, 从上下文代码中可以找到设置 RSA 公钥的函数 SetPublic, 在该函数下断点跟一下即可知道公钥 e 和 t.
1.2 i 参数分析
根据上文的代码截图可以知道 i 的来源.
关键代码:
o = $_BFo()["encrypt1"](de["stringify"](t["$_EJV"]), t["$_CCHU"]()),
i = p["$_HEt"](o)
o 参数生成
t["$_EJV"]是一个 Object, 保存了一些验证码的信息.
t["$_CCHU"]()
从上面的 i 参数知道是一个 aes key.
然后分析下 encrypt1 函数的逻辑, 简单查看代码逻辑后基本断定是 aes 加密算法而且从各种函数关键词来看基本不会是魔改 aes.
主动调用该函数确认是标准的 aes, 并且采用 cbc 模式/pkcs7 填充, iv 默认是 0000000000000000.
完成 aes 加密之后对每个 32 位的数字做如下转换
其作用是将 32 位的数字转为四个 8 位的数字
i 参数生成
i 由p["$_HEt"]
函数输入 o 经过一系列操作之后由 res+end 组成, 其中$_HCK
函数有多处将三个 8bit 数字换算成 24bit 数字然后转换为四个字符的逻辑, 似乎是 base64 算法变体.
t 函数是取指定 t 二进制位上对应的值组合成新的数字.
$_GJI
函数作用是取对应数字的字符, 取不到则用"."代替. $_GAp="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789()",
与标准 base64 需要的字符串差异在最后两个字符由"+/"变为了"()".
$_GCK=7274496(二进制: 11011110000000000000000), $_GDu=9483264(100100001011010000000000)
$\_GEk=19220(100101100010100),$_GFY=235(11101011)
实现标准 base64 需要的四个数字应该是:
0xfc0000(111111000000000000000000),
0x3f000(111111000000000000),
0xfc0(111111000000),
0x3f(111111),
跟标准 base64 实现有点不同.
1.3 总结分析
r 参数保存 aes 密钥, 采用 RSA 加密后取 16 进制, i 是 aes 加密数据后采用 base64 编码的字符串.
第二次 w 生成逻辑分析
第二个 w 同样是由fullpage.xxx.js生成的.
n["w"] = t["$_CEAN"]
跟踪下 t["$_CEAN"]的生成, 可以发现同样是采用 aes 加密, 不过加密的数据不同, 这个 r 参数当中有很多未知的参数, 似乎是一些环境检测的参数.
{"lang":"zh-cn","type":"fullpage","tt":"M6(*((1Sj((sM((","light":-1,"s":"c7c3e21112fe4f741921cb3e4ff9f7cb","h":"321f9af1e098233dbd03f250fd2b5e21","hh":"39bd9cad9e425c3a8f51610fd506e3b3","hi":"09eb21b3ae9542a9bc1e8b63b3d9a467","vip_order":-1,"ct":-1,"ep":{"v":"9.1.9-r8k4eq","te":false,"$_BBp":false,"ven":"Google Inc. (NVIDIA)","ren":"ANGLE (NVIDIA, NVIDIA GeForce RTX 3070 (0x00002488) Direct3D11 vs_5_0 ps_5_0, D3D11)","fp":null,"lp":null,"em":{"ph":0,"cp":0,"ek":"11","wd":1,"nt":0,"si":0,"sc":0},"tm":{"a":1711849377534,"b":1711849377617,"c":1711849377617,"d":0,"e":0,"f":1711849377539,"g":1711849377539,"h":1711849377539,"i":1711849377539,"j":1711849377539,"k":0,"l":1711849377547,"m":1711849377614,"n":1711849377615,"o":1711849377620,"p":1711849377751,"q":1711849377751,"r":1711849377752,"s":1711849378121,"t":1711849378121,"u":1711849378121},"dnf":"dnf","by":2},"passtime":8258,"rp":"d2d182b3ce6cf55f590e9ec11c9b1635","captcha_token":"549902629","otpj":"jm4jwcx7"}
搜一下其中关键词例如"hh"找到代码逻辑位置.
2.1 s 参数分析
关键代码H(p["$_HD_"](t))
, 其中 p["$HD"]根据上文分析已知是 base64 变体算法, t 是一个字符串, H 函数是标准 md5 算法.
t 这个字符串来自$_BICT
函数生成, $_BICT
函数用于收集鼠标操作事件并采用 base64 编码, base64 字符映射为"()*,-./0123456789:?@ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz~".
收集到的鼠标事件信息会进行一系列转换成二进制字符串后进行 base64 编码, 核心代码如下:
for (var t = [], n = [], r = [], o = [], i = 0, s = e["length"]; i < s; i += 1) {
var a = e[i],
_ = a["length"];
t["push"](a[0]), n["push"](2 === _ ? a[1] : a[2]), 3 === _ && (r["push"](a[1][0]), o["push"](a[1][1]));
}
var c = f(t) + d(n, !1) + d(r, !0) + d(o, !0),
l = c["length"];
其中 t 是鼠标事件数组, 例如: ["move", "move", "down", "move", "up"], 鼠标事件包括: move/down/up/scroll/focus/blur/unload.
n 是鼠标事件发生的毫秒级时间戳数组(例如: [1711952404794, 1711952404794, 1711952404798, 1711952404798, 1711952404798]).
如果传入该函数的 e 参数每个元素是一个长度 3 的数组, 即每个元素只记录了[事件类型, [x 坐标, y 坐标], 毫秒级时间戳]信息,例如: "move", [123, 456], 1711952404794], ["down", [123, 456], 1711952404798, 则 r 和 o 参数分别是记录 x 和 y 坐标的数组.
2.2 h 参数分析
n = i["$_BJDB"]["$_BICT"]();
["h", H(p["$_HD_"](n))];
i["$_BJDB"]["$_BICT"]
函数作用是检测一个 Object 对象(该对象为空 Object)指定的属性是否存在, 存在则返回该属性的值, 不存在则返回-1, 由此组成一个数组并用"magic data"字符串 joi 拼接这个数组得到. 检测的属性列表如下:
["textLength","HTMLLength","documentMode","A","ARTICLE","ASIDE","AUDIO","BASE","BUTTON","CANVAS","CODE","IFRAME","IMG","INPUT","LABEL","LINK","NAV","OBJECT","OL","PICTURE","PRE","SECTION","SELECT","SOURCE","SPAN","STYLE","TABLE","TEXTAREA","VIDEO","screenLeft","screenTop","screenAvailLeft","screenAvailTop","innerWidth","innerHeight","outerWidth","outerHeight","browserLanguage","browserLanguages","systemLanguage","devicePixelRatio","colorDepth","userAgent","cookieEnabled","netEnabled","screenWidth","screenHeight","screenAvailWidth","screenAvailHeight","localStorageEnabled","sessionStorageEnabled","indexedDBEnabled","CPUClass","platform","doNotTrack","timezone","canvas2DFP","canvas3DFP","plugins","maxTouchPoints","flashEnabled","javaEnabled","hardwareConcurrency","jsFonts","timestamp","performanceTiming","internalip","mediaDevices","DIV","P","UL","LI","SCRIPT","touchEvent"]
2.3 tt 参数分析
e = i["$_CAAG"]["$_BIBg"]();
tt 参数作用是根据服务器返回的 c 和 s 参数截取鼠标事件 base64 编码后的字符串, 可能是用于校验.
c 是一个纯数字字符串, 每两个字符代表一个 16 进制数字; s 是一个数组, 取其中下标 0, 2, 4 三个数字.
2.4 hh 参数分析
["hh", H(n)]
hh 参数是 n 的 md5 值, 跟 h 参数区别是 h 参数是 base64 编码 n 之后取 md5 值.
2.5 hi 参数分析
var e = t["$_BJDB"]["$_BIBg"]();
t["$_CCFY"] = e;
["hi", H(i["$_CCFY"])]
t["$_BJDB"]["$_BIBg"]
函数的作用与 2.2 h 参数分析中的 n 变量的生成相似, 只是组成的数组用"!!"字符串 join 拼接得到.
2.6 ep 参数分析
["ep", i["$_CEDy"]() || -1]
ep 参数应该是一些环境检测的汇总数据, 各属性可以在代码中看出来, 汇总如下:
属性名 |
作用 |
v |
版本号, 当前版本为 9.1.9-r8k4eq |
te |
是否支持 touchEvent |
$_BBp |
是否支持 mouseEvent 事件 |
ven |
WEBGL_debug_renderer_info 扩展的 UNMASKED_VENDOR_WEBGL |
ren |
WEBGL_debug_renderer_info 扩展的 UNMASKED_RENDERER_WEBGL |
fp |
null, 可能是记录第一个鼠标事件 |
lp |
null, 可能是记录最后一个鼠标事件 |
em |
用于检测运行环境中的各种属性, 一般 1 为表示有该属性, 0 表示没有. 其中 ph 表示phantom 是否在 window 中; cp 表示 callPhantom 是否在 window 属性中; ek 表示遍历一个 TypeError 对象的属性列表, 检测的属性列表为["line","column","lineNumber","columnNumber","fileName","message","number","description","sourceURL","stack"], 并用一个二进制数表示, 存在则为 1, 否则为 0, 例如 0000010001, 然后将该二进制数用 16 进制字符串表示, 例如"11"; wd 表示 webdriver 在 window 属性中且 webdriver 为 true, nt 表示__nightmare 是否在 window 属性中; si 表示_webdriverscriptfn 是否在 document 属性中; sc 表示$cdc_asdjflasutopfhvcZLmcfl是否在 document 属性中 |
tm |
记录 window.performance.timing 对象的各属性, a: navigationStart, b: unloadEventStart, c: unloadEventEnd, d: redirectStart, e: redirectEnd, f: fetchStart, g: domainLookupStart, h: domainLookupEnd, i: connectStart, j: connectEnd, k: secureConnectionStart, l: requestStart, m: responseStart, n: responseEnd, o: domLoading, p: domInteractive, q: domContentLoadedEventStart,r: domContentLoadedEventEnd, s: domComplete, t: loadEventStart, u: loadEventEnd |
2.7 captcha_token 参数分析
captcha_token 是一个校验值, 分别对 n 函数, o 函数和 e 变量计算 djb hash 得到, 其中 n 函数是计算 captcha_token 所处的函数, o 函数是计算 djb hash 的函数, e 是一个固定值, 如果发现程序进入到开始计算 djb hash 的过程中执行时间大于 100ms 则将 e 变更为"qwe".
2.8 passtime 参数分析
s = $_Gt() - rt;
["passtime", s || -1]
passtime 记录加载 js 的时间到现在的毫秒数.
2.9 rp 参数分析
["rp", H(o["gt"] + o["challenge"] + s)]]
rp 参数是 gt + challenge + passtime 的 md5 值
第三次 w 参数分析
第三个 w 由 click.xxx.js 生成, 利用ast简单去掉字符串替换和 unicode 编码后在全文中搜索"w"关键词, 找到一处 w 的生成, 随后打断点.
查看代码可以知道w由p+u组成, 这个加密模式与上文的w生成很相似, 基本模式都是: aes加密数据后base64 + rsa加密aes key得到.
3.1 u参数分析
var u = n["$_CAAz"]();
通过对上文fullpage.xxx.js的分析, 可以很快判断出此处的u也是rsa加密aes key得到hex字符串, rsa公钥也可以通过全局搜索迅速得到.
3.2 p参数分析
h = X["encrypt"](ae["stringify"](o), n["$_CABL"]());
p = w["$_EFO"](h);
X["encrypt"]
是aes加密算法, n["$_CABL"]()
是aes key. p用于存储加密后的o对象信息, o对象信息示例如下:
{"lang":"zh-cn","passtime":711,"a":"2942_8572,5487_9028","pic":"/captcha_v3/batch/v3/66355/2024-04-02T13/word/bd8c68ebff254a08be793bcff14a7479.jpg","tt":"M/d8Pj:E8(D(PjIE(UQ?O:9U)R.B,::B)O5:-A-:A*,1:::ggJ:.-A:..:Dg-)(?.NMbpE-9-_-:,O-M-N88-8M:*O4N8N7N(GPN8N0Wif06Lh)b.qqb.ij:UC-K-:*:/)-)UCM9n:-)/),c(BAb)6-M-8-0T)*c/:1K)D3),c(:-U-7M:-9?b9j)G)Y(,()9(((b51BBB,bb5,55i/)M)(NKY-2Vd-cE?q2d.:EEj9(-7b9cL11MB--Io2OMN-)DBOE,)(?d).GbU/(6GgM9i1-3JE-(Ln((p(((*(Aj((qc9()b,(((2BB9(,b(,((nb5,18(@-P0))Y-0b9qH)(@-N,VGUB)*),6R.M92*:g/H@OHcUT30ng0:jATC1?/)(U--BhAN*)(95?5E-*M9(E/(-iMEW-7*(0j(()p((((((","ep":{"ca":[{"x":684,"y":404,"t":1,"dt":777},{"x":713,"y":471,"t":1,"dt":192},{"x":880,"y":527,"t":3,"dt":454},{"x":716,"y":442,"t":1,"dt":348016},{"x":794,"y":456,"t":1,"dt":367},{"x":881,"y":512,"t":3,"dt":343}],"v":"3.1.0","$_FB":false,"me":true,"tm":{"a":1712036876180,"b":1712036876251,"c":1712036876251,"d":0,"e":0,"f":1712036876181,"g":1712036876181,"h":1712036876181,"i":1712036876181,"j":1712036876181,"k":0,"l":1712036876189,"m":1712036876248,"n":1712036876317,"o":1712036876253,"p":1712036876348,"q":1712036876348,"r":1712036876349,"s":1712036876681,"t":1712036876681,"u":1712036876681}},"h9s9":"1816378497","rp":"839fed64c9f55bd9ecc4faffdebb9a29"}
其中有多个字段需要继续深入分析下来源, 全文搜索"tt"
字符串, 找到o对象各种属性的代码生成位置, 打上断点重新开始.
3.2.1 o["a"]参数分析
o["a"]是传进来的参数e, 往回跟一下调用栈.
可以知道, o["a"]是由函数n["$_CBIQ"]["$_FAE"]()
生成的, 在该函数位置打断点分析.
n["$_CBIQ"]["$_FAE"]()
作用是遍历this["$_FC_"]
的_JIT
属性, 将每个元素的$_CEGt
和$_CEHV
属性拼接, 得到的数组join字符,
得到一个组合字符串.
全文搜一下关键词$_CEGt
或$_CEHV
找到设置这两个属性的代码位置分析下.
通过对 $_BFGo
函数上下文进行分析, 可以知道$_CEGt
和$_CEHV
分别表示鼠标点击的相对x(相对于验证框的位置)和相对y坐标, 其中$_CEGt
是Math.round(x相对坐标 100)得到, $_CEHV
是Math.round(y相对坐标 100)得到.
所以可以知道o["a"]是记录点击坐标的字符串, 例如"4182_5734,6759_7365".
3.2.2 o["tt"]参数分析
o["tt"]
参数与2.3的tt
参数生成算法一致, 此时记录的是整个验证过程的鼠标操作事件, 包括move/down/up.
3.2.3 o["ep"]参数分析
"ep": n["$_BJJj"]()
ep是一个object对象, 记录了多个值, 其中ca和tm参数要分析下. ca是鼠标点击事件的坐标和耗时的数组, 这个数组最后一个元素是点击确认按钮.
tm参数与2.6 ep参数分析中的tm参数一致, 都是记录 window.performance.timing 对象的各属性, a: navigationStart, b: unloadEventStart, c: unloadEventEnd, d: redirectStart, e: redirectEnd, f: fetchStart, g: domainLookupStart, h: domainLookupEnd, i: connectStart, j: connectEnd, k: secureConnectionStart, l: requestStart, m: responseStart, n: responseEnd, o: domLoading, p: domInteractive, q: domContentLoadedEventStart,r: domContentLoadedEventEnd, s: domComplete, t: loadEventStart, u: loadEventEnd.
3.2.4 o["h9s9"]参数分析
全文搜索发现没有h9s9关键词, 但是一定会在某个时间点给o附上h9s9属性, 直接在o定义完后加个proxy, 在设置h9s9时debugger:
o = new Proxy(o, {
set: function (data, key, value) {
if (key && key === "h9s9") {
debugger;
}
data[key] = value
return true
}
});
继续执行则停在了设置h9s9的位置上, 往回看调用栈找h9s9的逻辑代码.
可以知道h9s9在变量_
中就已经被设置好, 继续分析_
的生成逻辑.
在变量_
定义处打下断点, 逐步调试, 可以知道h9s9在经历window["_gct"](_)
函数调用后被设置, 继续跟踪window["_gct"](_)
函数发现js文件已经跳转到gct.xxx.js
, 去除掉字符串函数和unicode编码后overwrite content, 进入_gct
这个函数打断点分析.
发现会走到StJC
函数, 并且在其中一个步骤设置了h9s9.
查看下Rbfk
函数的代码逻辑, 是一个djb hash算法, 所以这个h9s9参数就是给StJC
和Rbfk
函数的代码签名, 如果这两个函数的代码被变更则h9s9会不同. h9s9参数跟2.7 captcha_token 参数分析的captcha_token参数作用类似.
3.2.5 o["rp"]参数分析
rp参数跟上文2.9 rp 参数分析提到的rp参数很类似, 可以明显看到四个魔术, 也是md5算法, 所以rp是r["gt"] + r["challenge"] + o["passtime"]组合的md5值.