声明
本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!
本文章未经许可禁止转载,禁止任何修改后二次传播,擅自使用本文讲解的技术而导致的任何意外,作者均不负责,若有侵权,请在公众号【K哥爬虫】联系作者立即删除!
逆向目标
-
目标:VK 登录验证码
-
网址:
aHR0cHM6Ly92ay5jb20v
抓包分析
打开网址,选择邮箱登录,随便输入一个未注册的邮箱号,比如 aaw2,方便触发风控验证。输入完点登录会重定向到新的登录页面,重新输入,正常会显示 账号未找到,多点几次就会触发风控,登录有两种验证,如下图所示,分别为一点即过的和滑动拼图,这拼图人都不好滑对:
若触发验证,auth.validateAccount 接口响应返回的 errcode 为 14,redirect_uri 中的 session_token 后续接口会用到:
该接口的请求参数中,login 为输入的邮箱号,client_id 是定值,device_id 加密生成,后文分析,auth_token 是登录重定向接口响应返回的:
not_robot_captcha 接口的响应内容中,show_captcha_type 为触发的验证码类型,滑动拼图为 slider,一点即过的为 checkbox,可以此区分触发的类型,captcha_settings 中的参数会用于后续获取图片的接口,其余部分后文分析:
captchaNotRobot.getContent 接口返回的图片链接,请求参数中的 adFp 如何加密生成,响应返回的 steps 有何作用,后文分析:
验证接口为 captchaNotRobot.check,请求参数中,debug_info 为固定值在 not_robot_captcha.js 文件中,hash 加密了一些环境参数,answer 编码了拼图的还原顺序,这些后文都会逐一分析:
-
参数值异常:
{"response":{"status":"ERROR"}}; -
还原顺序错误:
{"response":{"redirect":"","show_captcha_type":"slider","status":"BOT","success_token":""}}; -
验证通过:
{"response":{"redirect":"","show_captcha_type":"","status":"OK","success_token":"eyJ..."}}。
逆向分析
device_id
从 auth.validateAccount 接口跟栈到 auth.js 文件中,直接搜索 device_id 会发现有 100 个匹配项,一个个下断调试显然不现实。跟栈到下图处,此时的 s 中 device_id 已经生成了,s 对应 e.bodyParams,e 是传进来的参数,因此,在最前面下断:
刷新网页,即会断住,但此时还未传入 device_id,下步断点断到该值传入时为止:
向上跟栈到下图处,此时 NQ.deviceId 即 device_id 参数的值:
搜索 NQ 可定位到如下代码处,首次生成后会通过 localStorageService 存储到浏览器,代码中有很多类似的位置,这里不是生成前的位置,但是不影响分析,主要走到 Ln 中:
r = TQ.authLocalStorageServiceEverywhere ? NQ.deviceId : Ln();
跟进到 Ln() 中,逻辑如下:
function Ln() {
let e;
try {
e = localStorage.getItem(“deviceId”)
} catch (e) {}
if (!e) {
e = on();
try {
localStorage.setItem(“deviceId”, e)
} catch (e) {}
}
return e
}
remove 掉缓存中的 deviceId,再刷新网页,即会断到 on 中处:
on 中就是其算法的生成逻辑:
let t = “”
, n = 21;
for (; n–; )
t += “useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict”[64 * Math.random() | 0];
console.log(t);
adFp
adFp 是获取背景图片链接接口的加密参数,从该接口的堆栈跟到 not_robot_captcha.js 文件中,ctrl + s 局部搜索下 adFp,发现就一个位置,下断后重新获取验证图片即会断住。如下图所示,此时 window.rb_sync 中 adFp 的值已经生成了:
再回到 Network 看接口,ctrl + s 搜索下 adFp 参数的值,找到第一次生成的位置,出现在 /csp 接口的请求参数中:
blocked-uri 中的接口在首页 vk.com 生成二维码时就会触发,因此该值需要在首页调试,否则都是从封装的 IndexedDB 中取已存储的值,无法定位生成逻辑。
从 /fp/?id= 堆栈跟到 sync-loader.js 文件中,在 return e(r(t(n))); 处下断点,走的异步流程,刷新网页断住后单步往下跟,跟到下图处就会发现熟悉的 window.rb_sync,此时 id 的值还是 undefined:
接着往下跟,到下图处就会发现,i 即 adFp 参数的值,因为 o 为 undefined,所以生成逻辑就在 Kr() 中:
跟进去,逻辑如下:
const Kr = window.crypto ? function () {
var n = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : 21;
return crypto.getRandomValues(new Uint8Array(n)).reduce((function (n, t) {
return n + ((t &= 63) < 36 ? t.toString(36) : t < 62 ? (t - 26).toString(36).toUpperCase() : t > 62 ? “-” : “_”)
}
), “”)
}
也可用 python 复现:
def get_ad_fp(n=21):
result = []
for _ in range(n):
使用 os.urandom 生成密码学安全的随机字节
random_byte = ord(os.urandom(1))
t &= 63 (保留低6位)
t = random_byte & 63
JavaScript 的 toString(36) 逻辑
if t < 36:
使用 Python 的 base36 转换
if t < 10:
result.append(str(t))
else:
result.append(chr(ord(‘a’) + t - 10))
elif t < 62:
(t - 26).toString(36).toUpperCase()
实际上就是大写字母 A-Z
result.append(chr(ord(‘A’) + t - 36))
elif t > 62:
result.append(’-’)
else: # t == 62
result.append(’_’)
return ‘’.join(result)
browser_fp
从验证接口 captchaNotRobot.check 跟到 not_robot_captcha.js 文件中,搜索后发现仅有两处,都打上断点,刷新网页断住,n.analyticsModel.fingerprint 即 browser_fp 参数的值:
刷新网页,断到上面的 n.analyticsModel = e 处,此时 fingerprint 值还未生成,也就是生成流程在中间这一块了:
这里就不单步慢慢跟了,搜索 analyticsModel.fingerprint 定位到下图处,t.visitorId 就是目标值:
t 定位在上面一行,跟到 e.get 中去,发现就是 Of(this.components) 生成了 browser_fp 参数的值,this.components 是一堆环境参数,如 audio、canvas、plugins 等等:
Of 其实就是高度混淆后的 MurmurHash3(x64 128-bit)哈希算法的实现,特征点很多,以输出结构为例(4 × 32bit):
(“00000000” + (a[0] >>> 0).toString(16)).slice(-8)
(“00000000” + (a[1] >>> 0).toString(16)).slice(-8)
(“00000000” + (s[0] >>> 0).toString(16)).slice(-8)
(“00000000” + (s[1] >>> 0).toString(16)).slice(-8)
MurmurHash3 是一种非加密型哈希函数,由 Austin Appleby 在 2008 年设计。它以其高性能、良好的分布性和低碰撞率而闻名,广泛应用于哈希表、布隆过滤器、缓存键等场景。其比 MD5、SHA-1 等加密哈希快得多,但他不能称为加密算法,并非以安全为设计目标。
感兴趣的小伙伴可以去了解下该算法,网页抠出来的 Of 算法以及 python 复现的代码都会分享到知识星球中,以供学习交流。
connectionDownlink、connectionRtt 都是网络相关的参数,一个是下载速度或下行带宽,一个统计数据包从发送端到接收端再返回发送端所需的总时间,至此请求参数分析完成了。
图像识别
验证接口请求参数中的 answer 就和滑动距离、轨迹有关,其就定义在 browser_fp 下面,base64 编码:
wO(JSON.stringify({value: t}))
t 就是小图的移动路径,是怎么生成的呢?向上跟栈到下图处,this.checkResult 中的 e.value 就是 t 值,type 为 slider:
直接搜索 checkResult 即可定位到位置:
再网上跟栈就能到 const IS 中,这里就是滑动拼图的组件,分析这段代码,再结合 captchaNotRobot.getContent 接口返回的 steps,就能知道 t 的生成逻辑,对比如下:
// steps
const steps = [
5,13,2,24,1,19,20,5,6,1,14,5,22,24,1,20,16,24,6,8,
2,9,12,13,24,17,16,4,2,22,14,23,16,10,14,2,5,12,23,
15,24,21,17,2,13,18,22,8,2,9,0,8,19,14,9,2,16,15,10,
23,3,21,16,2,13,20,15,0,14,16,4,16,1,5,11,2,24,2,19,
23,16,3,22,7,2,7,19,13,14,19,20,1,9,22,17,16,14,14,7,2
];
// 滑动正确的结果
const t = [
13,2,24,1,19,20,5,6,1,14,5,22,24,1,20,16,24,6,8,2,
9,12,13,24,17,16,4,2,22,14,23,16,10,14,2,5,12,23,15,
24,21,17,2,13,18,22,8,2,9,0,8,19,14,9,2,16,15,10,23,
3,21,16,2,13,20,15,0,14,16,4
];
综上,根据 steps 路径移动小图,移到拼成的点为止,截取 steps 就能得到正确的 t 值:
// 拖动滑块的距离
const sliderValue = 36; // 0 - 49
// 生成路径 t
// P = i.slice(0, 2 * w)
const t = steps.slice(1, sliderValue % 2 === 0 ? 2 * sliderValue - 1 : 2 * sliderValue + 1);
// t = steps[1: (2 * slider_value - 1) if slider_value % 2 == 0 else (2 * slider_value + 1)]
剩下的就是需要分析,移动到哪拼图能被还原,找到 “还原点”。
我们将图片切成 25 个块(5×5),根据分析,拖动滑块,每次都是按照移动序列执行 “两两 swap”(1 2、3 4、5 6),每一步都计算整张图的边缘总不连续度:
-
右邻边:block[i].right vs block[i+1].left
-
下邻边:block[i].bottom vs block[i+5].top
找全程最优状态,当整张图的边缘误差达到全局最小值时,就说明所有块都回到了原始相对位置,这一步就是 “还原点”。
连续性评分,公式描述如下:
相关还原算法会分享到知识星球中,以供学习交流,还原效果如下:
结果验证
共同学习,写下你的评论
评论加载中...
作者其他优质文章






















