本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!
// 官网体验地址 aHR0cHM6Ly93d3cuaXNodW1laS5jb20vdHJpYWwvY2FwdGNoYS5odG1s // 官方隐藏地址 aHR0cHM6Ly9jYXN0YXRpYy5mZW5na29uZ2Nsb3VkLmNuL3ByL3YxLjAuNC9kZW1vLmh0bWw= // 某红书验证页面 aHR0cHM6Ly93d3cueGlhb2hvbmdzaHUuY29tL3dlYi1sb2dpbi9jYXB0Y2hh
数美不同类型验证码核心的 JS 都是一样的,只是个别参数有微小差别,主要以滑块为例来分析,通过 JS 代码以及官方文档可以看出数美是有无感验证的,但是官网体验地址里并没有放出来,官方有一个隐藏地址,里面的 demo 是最全的,包括无感,可以去上面给出的第二个地址里查看;数美的加密参数包含了 DES 加密算法,参数名以及 DES Key 不定时会变化,本文也会分析如何利用 AST 来获取动态的参数。
conf
接口,获取配置,主要是获取核心的 captcha-sdk.min.js
的地址,请求参数解释:
参数 | 含义 |
---|---|
organization |
数美分配的公司标识,一般是每个网站唯一,写死即可 |
appId |
应用标识,区分不同应用,数美后台可以管理 |
callback |
回调参数 |
lang |
语言,zh-cn 简体中文、zh-tw 繁体中文、en 英文 |
model |
模式,slide 滑块、auto_slide 无感验证、select 文字点选、icon_select 图标点选、seq_select 语序点选、spatial_select 空间推理 |
sdkver |
这个 sdk 版本是 captcha-sdk.min.js 内部写死的 |
channel |
推广渠道,数美后台可以管理 |
captchaUuid |
32位随机字符串,与业务方自身埋点数据配合,便于后续定位问题或进行数据统计 |
rversion |
captcha-sdk.min.js 版本号 |
返回结果重点看 captcha-sdk.min.js
文件地址,如下图所示有个 v1.0.4-171
,本文中我们称 v1.0.4
为大版本,171
为小版本,小版本不定时会更新,版本号不断升高。
然后就是 register
接口,不同类型,返回的数据都大同小异,其中 bg
是背景图片,fg
是滑块,文字点选、空间推理中 order
是提示信息,k
、l
、rid
三个参数后续会用到。
最后就是 fverify
验证接口,有类似下图红框中的 12 个参数,都是通过 JS 生成的,其参数名会根据 captcha-sdk.min.js
的变化而变化,其中有个最长的类似于下图的 ep
值,包含了轨迹加密。返回值里参数解释:
参数 | 含义 |
---|---|
code |
1100 :成功;1901 :QPS超限;1902 :参数不合法;1903 :服务失败;9101 :无权限操作 |
riskLevel |
处置建议,PASS :正常,建议直接放行;REJECT :违规,建议直接拦截 |
跟栈会发现核心逻辑在 captcha-sdk.min.js
里,这个 JS 类似于 OB 混淆(以前的文章介绍过,此处不再细说):
这里可以自己写 AST 还原一下,为了方便我们直接使用 v_jstools 解混淆:
然后替换掉原来的 captcha-sdk.min.js
,如果你测试的是官网的体验页面,使用 Fiddler 替换时要注意可能有跨域问题,需要利用 Filters 功能,设置响应头 Access-Control-Allow-Origin
字段值为当前域名:
如果你没注意到这个跨域问题,可能会替换之后发现没替换成功,原因是数美的资源有四个域名,其中一个宕了便会启用另一个,你替换其中一个报错了就会自动跳转另一个,所以看起来你并没有替换成功:
PS:若替换的 JS 格式化了,那么你在网页上滑动也是校验失败的,因为 JS 里检测了格式化,将 JS 压缩成一行再替换即可,具体检测的位置后文会讲到。
直接搜索关键词下断点,经过多次调试会发现第一个出现 captchaUuid
的地方是在 smcp.min.js
,如下图所示:
这里的栈并不多,来回跟栈也没发现是哪里生成的,此时可以从初始位置也就是 embed.html
初始化验证码的地方开始单步跟:
单步跟进去会发现一个 getCaptchaUuid()
的方法,将此方法扣出来即可。
function generateTimeFormat() { var e = new Date() , t = function(n) { return +n < 10 ? "0" + n : n.toString(); }; return ((e.getFullYear().toString() + t(e.getMonth() + 1)) + t(e.getDate()) + t(e.getHours()) + t(e.getMinutes())) + t(e.getSeconds()); } function getCaptchaUuid() { var c = ""; var o = "ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678"; var s = o.length; for (var a = 0; a < 18; a++) { c += o.charAt(Math.floor(Math.random() * s)); } return generateTimeFormat() + c; }
直接跟栈就很容易找到,如下图所示的位置,D 就是生成的所有参数,此外,也可以通过搜索关键字 getEncryptContent
或者直接搜索参数名称来定位。
可以发现上图里就有四个加密参数,都用到了 getEncryptContent
这个加密方法,加密方法传入两个参数,一个是待加密参数,一个是 DES Key,这四个待加密参数分别为 appId
值、channel
值、lang
值和一个 getSafeParams
方法。
重点跟进 getEncryptContent
方法看看,一个控制流,挑几个重点的讲一下,第一步是获取一个 key
,这个 key
是在前面设置的,后续会讲到,实际上这个 key
没啥用。
然后会有一个 isJsFormat
的格式化检测函数,正常应该是 false 的,如果你格式化了就为 true,也就会导致 f 的值为时间戳加数美的域名,这个 f 值后续是 DES 的 Key,不对的话自然怎么滑都不会通过。
然后就是 DES 加密了,这个 DES 是标准的加密算法,下图中传入的 1 和 0 表示的是加密,0 和 0 则表示解密,解密的情况也有,后续会遇到,mode
为 ECB
,padding
为 ZeroPadding
,不需要 iv
,可以直接扣代码,或者直接引库即可。
var CryptoJS = require("crypto-js") function DESEncrypt(key, word) { var key_ = CryptoJS.enc.Utf8.parse(key); var srcs = CryptoJS.enc.Utf8.parse(word); var encrypted = CryptoJS.DES.encrypt(srcs, key_, { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.ZeroPadding }); return encrypted.toString(); } function DESDecrypt(key, word) { var key_ = CryptoJS.enc.Utf8.parse(key); var decrypt = CryptoJS.DES.decrypt(word, key_, { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.ZeroPadding }); return decrypt.toString(CryptoJS.enc.Utf8); }
这里的四个值就分析完了,还有八个值是在前面生成的,如下图所示 x 的值即为其他八个值,往前看是一个函数生成的,往里面跟即可。
跟进来是一个 getMouseAction
方法,里面先是挨个取值,后续会对这些值进行 DES 加密,下图中的 a、c 参数就是 register
接口返回的 k、l 值,s 参数是对 register
接口返回的 k 值进行解密操作:
上图中 u = this._data
里面的值,根据滑块、点选、无感模式的不同,也有所差异,以下代码中,以 baseData
来表示 this._data
的值,根据模式的不同,可分为三类,大致构成如下:
滑块(slide
):
/* track:滑动轨迹(x, y, t),distance:滑动距离,randomNum:生成两数之间的随机值,示例: var track = [[0, -2, 0], [62, 1, 98], [73, 4, 205], [91, 3, 303], [123, -3, 397], [136, 8, 502], [160, 0, 599], [184, 0, 697], [169, 0, 797]] var distance = 169 */ var baseData = {} baseData.mouseData = track baseData.startTime = 0 baseData.endTime = track[track.length - 1][2] + randomNum(100, 500) baseData.mouseEndX = distance baseData.trueWidth = 300 baseData.trueHeight = 150 baseData.selectData = [] baseData.blockWidth = 40
滑块轨迹生成代码:
def get_sm_track(distance): track_length = random.randint(4, 10) track = [[0, -2, 0]] m = distance % track_length e = int(distance / track_length) for i in range(track_length): x = (i + 1) * e + m + random.randint(20, 40) y = -2 + (random.randint(-1, 10)) t = (i + 1) * 100 + random.randint(-3, 5) if i == track_length - 1: x = distance track.append([x, y, t]) else: track.append([x, y, t]) logger.info("track: %s" % track) return track
点选类(文字点选 select
、图标点选 icon_select
、语序点选 seq_select
、空间推理 spatial_select
):
/* coordinate:点选坐标(x, y),randomNum:生成两数之间的随机值,示例: var coordinate = [[171, 101], [88, 102], [138, 109], [225, 100]] */ var baseData = {} var time_ = new Date().getTime() coordinate.forEach(function(co) { co[0] = co[0] / 300 co[1] = co[1] / 150 co[2] = time_ time_ += randomNum(100, 500) }) baseData.mouseData = coordinate baseData.startTime = time_ - randomNum(800, 20000) baseData.endTime = coordinate[coordinate.length - 1][2] baseData.mouseEndX = 0 baseData.trueWidth = 300 baseData.trueHeight = 150 baseData.selectData = coordinate baseData.blockWidth = undefined
无感(auto_slide
):
/* randomNum:生成两数之间的随机值 */ var baseData = {} baseData.mouseData = [[0, 0, 0]] baseData.startTime = 0 baseData.endTime = randomNum(100, 500) baseData.mouseEndX = 260 baseData.trueWidth = 300 baseData.trueHeight = 150 baseData.selectData = [] baseData.blockWidth = 40
这些值生成完了之后,就是挨个通过 getEncryptContent
进行加密,前面已经分析过,实际上就是 DES 加密,可以看到分为点选、滑块和无感三类,其中 DES Key 也是会每隔一段时间变化的:
再往下走还有三个加密参数,待加密值是定值,然后将 s 的值(也就是前面 register
接口返回的 k 经过 DES 解密后的值赋值给了 this._data.__key
)。
至此所有加密参数就搞完了。
前面说了,/v1.0.4-171/captcha-sdk.min.js
文件地址,我们称 v1.0.4
为大版本,171
为小版本,小版本每隔一段时间会更新,版本号会不断升高,具体更新周期是多少?这里推荐一个方法 document.lastModified
,该方法记录的是物理网页的最后修改时间,我们直接访问 JS 地址,就可以直接查看不同版本的 JS 是啥时候更新的了,多对比几个版本,发现更新间隔时间并没有太明显的规律,如下图所示:
不同版本里面的 12 个加密参数的名称和 DES 加密的 Key 都不一样,我们可以利用 AST 来动态获取这 12 个参数,经过测试,以下版本均可正常提取:
v1.0.4-148
~ v1.0.4-171
v1.0.3-147
~ v1.0.3-171
v1.0.1-147
~ v1.0.1-171
PS:此 AST 代码仅实现对动态参数的提取,并非还原所有的混淆,提取出来的结果是有序、未去重的,后续按索引取就行。